《A Tour of C++》要点收录

一、基础知识

2023-10-15  本文已影响0人  akuan

Link

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

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

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


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

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

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

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

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

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


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


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,“特定原因”包括:


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

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)就是这么实现的。

忠告

上一篇 下一篇

猜你喜欢

热点阅读