一、基础知识

作者: akuan | 来源:发表于2023-10-15 09:40 被阅读0次

    Link

    C++是种编译型语言。要运行一个程序,其源文本需要通过编译器处理, 生成一些目标文件,再经链接器组合后给出一个可执行文件。

    ISO C++ 标准定义了两类东西:

    • 语言核心特性, 诸如内置的类型(例如 char 和 int) 以及循环(例如 for-语句 和 while-语句)
    • 标准库组件, 诸如容器(例如 vector 和 map) 以及 I/O 操作(例如 << 和 getline())。标准库组件完全是普通的 C++ 代码,由具体的 C++ 实现提供。 换句话说, C++ 标准库可以且确实是用 C++ 自身 (包括少量的机器语言代码,用于线程上下文切换等功能)实现的。

    C++ 是静态类型语言。 就是说,任何一个东西(例如对象、值、名称和表达式)被用到的时候, 编译器都【必须】已经知晓其类型。对象的类型确定了可施加操作的集合。


    如果在 C++ 程序里要做一件事,主要的方式是调用某个函数去执行它。 定义函数就是指定某个操作怎样被执行。 除非事先声明过,否则函数无法被调用

    函数声明给出了该函数的名称、返回值类型(如果有的话)、 以及调用它时必须提供的参数数量和类型。

    函数声明中可以包含参数名。 这对程序的读者有益,但除非该声明同时也是函数定义,编译器将忽略这些参数名。例如下面两种声明实质是一样的:

    double sqrt(double d); // 返回 d 的平方根
    double square(double); // 返回参数的平方
    

    函数可以作为类的成员。 对于成员函数(member function)来说,类名也是该函数类型的组成部分。例如:

    char& String::operator[](int index);
    


    声明是把一个实体引入程序的语句。它规定了这个实体的类型:

    • 类型(type) 规定了一组可能的值和一组(针对对象的)运算;
    • 对象(object) 是一块内存,其中承载某种类型的值;
    • (value) 是一些二进制位,其含义由某个类型规定;
    • 变量(variable) 是一个具名对象。


    C++ 有多种初始化方法,比如上面用到的 =, 还有一种通用形式,基于花括号内被隔开的初值列表:

    double d1 = 2.3;// d1 初始化为 2.3
    double d2 {2.3};// d2 初始化为 2.3
    double d3 = {2.3};// d3 初始化为 2.3(使用 { ... } 时,此处的 = 可有可无)
    complex<double> z = 1;// 一个复数,使用双精度浮点数作为标量
    complex<double> z2 {d1,d2};
    complex<double> z3 = {d1,d2};// (使用 { ... } 时,此处的 = 可有可无)
    vector<int> v {1,2,3,4,5,6};// 一个承载 int 的 vector
    

    没有特定原因去指明类型时,就可以用auto,“特定原因”包括:

    • 如果该定义处在较大的作用域中,希望其类型对阅读源码读的人一目了然。
    • 希望明确规定变量的取值范围或精度(比方说,想用double,而非float)。


    关于不可变更,C++有两种概念:

    • const:相当于“我保证不会修改这个值”。 它主要用于指定接口,对于通过指针以及引用传入函数的数据,无需担心其被修改。 编译器为const作出的“保证”担保。一个const的值可在运行期间得出。

    • constexpr:相当于“将在编译期估值”。 它主要用于定义常量,指定该数据被置于只读内存(在这里被损坏的几率极低)中, 并且在性能方面有益。 constexpr的值必须由编译器算出

    constexpr double square(double x) { return x*x; }
    int var = 17;
    constexpr double max1 = 1.4*square(17);// OK 1.4*square(17) 是常量表达式,可在【编译期】估值
    constexpr double max2 = 1.4*square(var);// 报错:var不是常量表达式
    const double max3 = 1.4*square(var);// OK,可在【运行时】估值
    

    要成为constexpr,函数必须极其简单,且不能有副作用,且只能以传入的数据作为参数。 尤其是,它不能修改非局部变量,但里面可以有循环,以及它自己的局部变量。例如:

    constexpr double nth(double x, int n) {// 假定 n>=0
        double res = 1;
        int i = 0;
        while (i<n) {
            res*=x;
            ++i;
        }
        return res;
    }
    

    在某些场合下,语言规则强制要求使用常量表达式(比如:数组界限、case标签、模板的值参数,以及用constexpr定义的常量)。其它情况下,编译期估值都侧重于性能方面。 抛开性能问题不谈,不变性(状态不可变更的对象)是一个重要的设计考量。

    char v[6];  // 6个字符的数组
    char* p;    // 指向字符的指针
    

    在声明里,[]的意思是“什么什么的数组”,而*的意思是“指向什么什么东西”。

    char* p = &v[3];    // p指向v的第四个元素
    char x = *p;        // *p是p指向的对象
    

    以上,v有六个元素,从v[0]到v[5]。指针变量p可持有相应类型对象的地址。

    在表达式里,一元前置运算符*的意思是“什么什么的内容”, 而一元前置运算符&的意思是“什么什么的地址”。我们可以把前面初始化定义的结果图示如下:

    如下示例是将数组v中的所有元素加一,若不想把v中的值复制到变量x,而是仅让x引用一个元素:

    void increment() {
        int v[] = {0,1,2,3,4,5,6,7,8,9};
        for (auto& x : v)// 为v里的每个x加1
            ++x;
    }
    

    在声明中,一元前置运算符&的意思是“引用到什么什么”。 引用和指针类似,只是在访问引用指向的值时,无需前缀*。 此外,在初始化之后,引用无法再指向另一个对象

    在定义函数参数时,引用就特别有价值。例如:

    void sort(vector<double>& v);
    

    通过引用,我们确保了在调用sort(my_vec)的时候,不会复制my_vec, 并且被排序的确实是my_vec,而非其副本。

    想要不修改参数,同时还避免复制的开销,可以用const引用。接收const引用参数的函数很常见。

    运算符(例如&*[])用在声明中的时候, 被称为声明运算符(declarator operator):

    T a[n]  // T[n]: 具有n个T的数组
    T* p    // T*: p是指向T的指针
    T& r    // T&: r是指向T的引用
    T f(A)  // T(A): f是个函数,接收一个A类型的参数,返回T类型的结果
    

    在老式代码里,通常用0NULL,而非nullptr。 但是,采用nullptr, 可以消除整数(比如0或NULL)和指针(比如nullptr)之间的混淆。
    对指针指的判定(比如if(p)),等同于将其与nullptr比较(也就是if(p!=nullptr))。


    初始化和赋值不一样。 一般来说,想要让赋值操作正确运行,被赋值对象必须已经有一个值。 另一边,初始化的任务是让一块未初始化过的内存成为一个有效的对象。 对绝大多数类型来说,针对 未初始化变量 的读取和写入都是未定义的(undefined)。 对于内置类型,这在引用身上尤其明显:

    int x = 7;
    int& r {x}; // 把r绑定到x上(r引用向x)
    r = 7;      // 不论r引用向什么,给它赋值
    int& r2;    // 报错:未初始化引用
    r2 = 99;    // 不论r2引用向什么,给它赋值
    

    很幸运,不存在未初始化的引用; 如果能,那么r2=99就会把99赋值给某个不确定的内存位置; 其结果会导致故障或者崩溃。

    =可用于初始化引用,但千万别被它搞糊涂了。例如:

    int& r = x; // 把r绑定到x上(r引用向x)
    

    这依然是初始化r,并把它绑定到x上,而不涉及任何的值复制操作。

    初始化和赋值的区别,对很多用户定义的类型 ——比如string和vector——而言同样极度重要, 在这些类型中,被赋值的对象拥有一份资源,该资源最终将被释放。

    参数传递和返回值返回的基本语义是初始化。举例来说,传引用(pass-by-reference)就是这么实现的。

    忠告

    • [1] 别慌!船到桥头自然直;
    • [2] 不要专门或单独使用内置特性。 恰恰相反,基本(内置)特性,最好借助程序库间接使用, 比方说 ISO C++ 标准库(第8-15章);
    • [3] 想写出好程序,不必对C++掌握到巨细靡遗。
    • [4] 把力气用在编程技术上,别死磕语言特性。
    • [5] 有关语言定义相关问题的最终解释, 请参考 ISO C++ 标准;
    • [6] 把有用的操作“打包”成函数,再取个好名字;
    • [7] 函数应当仅具有单一的逻辑功能;
    • [8] 保持函数简短;
    • [9] 当函数针对不同类型执行同样概念的操作时,请采用重载;
    • [10] 当函数可能在编译期估值时,用constexpr声明它;
    • [11] 去理解基本语义向硬件的映射;
    • [12] 用数字分隔符为大文本值提高可读性;
    • [13] 不要使用复杂表达式;
    • [14] 不要使用导致范围缩小的类型转换;
    • [15] 尽量让变量的作用域保持最小;
    • [16] 不要使用“魔数”;使用符号常量;
    • [17] 尽量用不可变更的数据;
    • [18] 每个声明里有(且仅有)一个名称;
    • [19] 保持常见和局部名称简短,让不常见和非局部名称长一些;
    • [20] 不要使用形似的名称;
    • [21] 不要使用全大写(ALL_CAPS)名称;
    • [22] 在提及类型的声明里,尽量用{}-初始化 语法;
    • [23] 使用auto以避免重复输入类型名;
    • [24] 尽量别弄出来未初始化的变量;
    • [25] 尽量缩小作用域;
    • [26] 如果在if-语句的条件中定义变量,尽量采用针对0的隐式判定;
    • [27] 仅在涉及位操作时,使用unsigned
    • [28] 确保对指针的使用简单且直白;
    • [29] 用nullptr,而非0NULL
    • [30] 在有值去初始化它之前,别声明变量;
    • [31] 别给直观的代码写注释;
    • [32] 用注释阐释意图;
    • [33] 保持缩进风格一致。

    相关文章

      网友评论

        本文标题:一、基础知识

        本文链接:https://www.haomeiwen.com/subject/rudwbdtx.html