C++基础

《C++ Primer》第一部分

Posted by ZhouSh on January 14, 2023

一、开始

main函数的返回类型必须为int。在大多数系统中,main的返回值被用来指示状态。返回0表示成功,非0的含义由系统定义,通常用来指出错误类型。

endl是一个被称为操纵符的特殊值。写入endl的效果是结束当前行,并将与设备关联的缓冲区(buffer)中的内容刷到设备中。缓冲刷新操作可以保证到目前为止程序所产生的所有输出都真正写入输出流中,而不是仅停留在内存中等待写入流。程序员常常在调试时添加打印语句。这类语句应该保证“一直”刷新流。否则,如果程序崩溃,输出可能还留在缓冲区中,从而导致关于程序崩溃位置的错误推断。

调试时打印语句都要增加endl。

虽然编译器会忽略注释,但读者并不会。即使系统文档的其他部分已经过时,程序员也倾向于相信注释的内容是正确可信的。因此,错误的注释比完全没有注释更糟糕,因为它会误导读者。因此,当你修改代码时,不要忘记同时更新注释

在Windows系统中,输入文件结束符的方法是敲Ctrl+Z(按住Ctrl键的同时按Z键),然后按Enter或Return键。在UNIX系统中,文件结束符输入是用Ctrl+D。

include来自标准库的头文件时,也应该用尖括号(<>)包围头文件名。对于不属于标准库的头文件,则用双引号(” “)包围。

二、变量和基本类型

C++标准规定的算术类型的尺寸的最小值,同时允许编译器赋予这些类型更大的尺寸。C++语言规定一个int至少和一个short一样大,一个long至少和一个int一样大,一个long long至少和一个long一样大。其中,数据类型long long是在C++11中新定义的。

建议:如何选择类型

  1. 使用int执行整数运算。在实际应用中,short常常显得太小而long一般和int有一样的尺寸。如果你的数值超过了int的表示范围,选用long long。
  2. 使用double执行浮点数运算。这是因为float通常精度不够而且双精度浮点数和单精度浮点数的计算代价相差无几。事实上,对于某些机器来说,双精度运算甚至比单精度还快。

初始化不是赋值,初始化的含义是创建变量时赋予其一个初始值,而赋值的含义是把对象的当前值擦除,而以一个新值来替代。

为了支持分离式编译,C++语言将声明和定义区分开来。声明(declaration)使得名字为程序所知,一个文件如果想使用别处定义的名字则必须包含对那个名字的声明。而定义(definition)负责创建与名字关联的实体。

如果想声明一个变量而非定义它,就在变量名前添加关键字extern,而且不要显式地初始化变量。任何包含了显式初始化的声明即成为定义。在函数体内部,如果试图初始化一个由extern关键字标记的变量,将引发错误。

1
2
3
extern int i;     // 声明而非定义
int i;            // 声明并定义
extern int i = 0; // 定义

变量能且只能被定义一次,但是可以被多次声明。声明和定义的区别看起来也许微不足道,但实际上却非常重要。如果要在多个文件中使用同一个变量,就必须将声明和定义分离。此时,变量的定义必须出现在且只能出现在一个文件中,而其他用到该变量的文件必须对其进行声明,却绝对不能重复定义。

用户自定义的标识符中不能连续出现两个下划线,也不能以下划线紧连大写字母开头。此外,定义在函数体外的标识符不能以下划线开头。

C++11中新增了一种引用:所谓的“右值引用(rvalue reference)”,后面介绍,这种引用主要用于内置类。严格来说,当我们使用术语“引用(reference)”时,指的其实是“左值引用(lvalue reference)”。引用即别名,必须初始化。

因为const对象一旦创建后其值就不能再改变,所以const对象必须初始化。默认状态下,const对象仅在文件内有效。当多个文件中出现了同名的const变量时,其实等同于在不同文件中分别定义了独立的变量。如果想在多个文件之间共享const对象,必须在变量的定义之前添加extern关键字来声明。

很多新手程序员经常忘了在类定义的最后加上分号。类体后面可以紧跟变量名以示对该类型对象的定义,所以分号必不可少。

三、字符串、向量和数组

把0到15之间的十进制数转换成对应的十六进制形式,只需初始化一个字符串令其存放16个十六进制“数字”:

1
2
3
4
5
6
7
8
9
const string hexdigits = "0123456789ABCDEF"; // 可能的十六进制数字
string result;                               // 用于保存十六进制的字符串
string::size_type n;                         // 用于保存从输入流读取的数
while (cin >> n)
{
  if (n < hexdigits.size()) // 忽略无效输入
    result += hexdigits[n];
}
cout << "your hex number is: " << result << endl;

不能在范围for循环(for(auto a:A))中向vector对象添加元素。任何一种可能改变vector对象容量的操作,比如push_back,都会使该vector对象的迭代器失效。

1
2
3
4
5
6
7
vector<int> A = {1, 2, 3};
for (auto a : A)
{
  A.push_back(1);
  cout << a << ", ";
}
cout << endl;

输出:1, 0, 1781690384,

使用迭代器完成二分搜索:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
auto beg = text.begin, end = text.end();
auto mid = text.begin() + (end - beg) / 2;
while (mid != end && *mid != sought) // 当还有元素尚未检查且还没有找到sought时执行循环
{
  if (sought < *mid)
  {
    end = mid;
  }
  else
  {
    beg = mid + 1;
  }
  mid = beg + (end - beg) / 2;
}

为什么用的是mid=beg+(end-beg)/2,而非mid=(beg + end)/2

  1. 前者不会产生溢出,而后者可能会
  2. 前者适用于对迭代器的操作,而后者不行(迭代器有相减没有相加)

字符数组有一种额外的初始化形式,我们可以用字符串字面值对此类数组初始化。当使用这种方式时,一定要注意字符串字面值的结尾处还有一个空字符,这个空字符也会像字符串的其他字符一样被拷贝到字符数组中去。如const char a[3]="abc"是错的,数组大小必须是4位,还有1位存放结尾处的空字符。

不能将数组的内容拷贝给其他数组作为其初始值,也不能用数组为其他数组赋值。

在很多用到数组名字的地方,编译器都会自动地将其替换为一个指向数组首元素的指针

现代的C++程序应当尽量使用vector和迭代器,避免使用内置数组和指针;应该尽量使用string,避免使用C风格的基于数组的字符串。

使用范围for语句处理多维数组。将外层循环的控制变量row声明成了引用类型,这是为了避免数组被自动转成指针,否则无法通过编译。如果row不是引用类型,编译器初始化row时会自动将这些数组形式的元素(和其他类型的数组一样)转换成指向该数组内首元素的指针。这样得到的row的类型就是int*,显然内层的循环就不合法了。

1
2
3
4
5
6
7
int a[2][3] = {1, 2, 3, 4, 5, 6};
int b = 0;
for (auto &row : a)
  for (auto col : row)
  {
    cout << col << endl;
  }

四、表达式

左值和右值:当一个对象被用作右值的时候,用的是对象的值(内容);当对象被用作左值的时候,用的是对象的身份(在内存中的位置)。在需要右值的地方可以用左值来代替,但是不能把右值当成左值(也就是位置)使用。当一个左值被当成右值使用时,实际使用的是它的内容(值)。

一元负号运算符对运算对象值取负后,返回其(提升后的)副本。如bool取负返回提升成int的副本,所以下面b2的结果为true,打印为1。(b1=1,b2=-1。bool是0为false,其他为true,所以b2转bool为true。)

1
2
3
bool b1 = true;
bool b2 = -b1;
cout << b2 << endl;

C++11新标准则规定整数除法的商一律向0取整(即直接切除小数部分)。

取余:如果m%n不等于0,则它的符号和m相同。

关系运算符的求值结果是布尔类型。所以if(i<j<k)是将i<j的结果(0或1)与k比较,应写为if(i<j && j<k)

建议:除非必须,否则不用i++,而应该用++i。

逗号运算符:首先对左侧的表达式求值,然后将求值结果丢弃掉。逗号运算符真正的结果是右侧表达式的值。

强制类型转换:static_cast、dynamic_cast、const_cast、reinterpret_cast

img

五、语句

try catch:C++标准库定义了一组类,用于报告标准库函数遇到的问题。然后用try{}catch(exception){}捕获异常。 img

六、函数

IO类型不支持拷贝操作,因此函数只能通过引用形参访问该类型的对象。

局部静态对象:可以将局部变量定义成static类型,在程序的执行路径第一次经过对象定义语句时初始化,并且直到程序终止才被销毁。如下面这个程序将输出1~10的数字。

1
2
3
4
5
6
7
8
9
10
11
12
int f()
{
    static int a = 0;
    return ++a;
}

int main()
{
    for (int i = 0; i != 10; ++i)
        cout << f() << endl;
    return 0;
}

尽量使用常量引用

数组的特殊性质不允许拷贝数组,所以无法以值传递的方式使用数组参数。使用数组时(通常)会将其转换为指针,所以当我们为函数传递一个数组时,实际上时指向数组首元素的指针。

不要返回局部对象的引用或指针。函数完成后,局部对象所占用的储存空间也将随之释放。

因为数组不能被拷贝,所以函数不能返回数组。但是函数可以返回数组的指针或引用。

当我们把函数名作为一个值使用时,该函数自动地转换成指针。

七、类

成员函数通过一个名为this的额外的隐式参数来访问调用它的那个对象,this形参是隐式定义的。实际上,任何自定义名为this的参数或变量的行为都是非法的

编译器首先编译成员的声明,然后才轮到成员函数体。因此,成员函数体可以随意使用类中的其他成员而无须在意这些成员出现的次序。

构造函数不能被声明成const,当我们创建类的一个const对象时,直到构造函数完成初始化过程,对象才能真正取得其“常量”属性。因此,构造函数在const对象的构造过程中可以向其写值。

class的成员默认为private,struct的成员默认为public。使用class和struct定义类唯一的区别就是默认的访问权限

类可以允许其他类或者函数访问它的非公有成员,方法是令其他类或者函数成为它的友元(friend)。如果类想把一个函数作为它的友元,只需要增加一条以friend关键字开始的函数声明语句即可。

友元的声明仅仅指定了访问的权限,而非一个通常意义上的函数声明。如果我们希望类的用户能够调用某个友元函数,那么我们就必须在友元声明之外再专门对函数进行一次声明。

为了使友元对类的用户可见,我们通常把友元的声明与类本身放置在同一个头文件中(类的外部)。

定义(不是声明)在类内部的成员函数是自动inline的。

若希望在const成员函数内修改某个数据成员,可以通过在变量的声明中加入mutable关键字。

一个类的成员类型不能是该类自己,但可以是该类自己的指针或引用。因为只有当类成员全部完成后类才算被定义。但一个类的名字出现后,它就被认为是声明过了,只要类被声明过,就可以定义指针或引用指向它。

友元函数能定义在类的内部,这样的函数是隐式内联的。

如果一个类指定了友元类,则友元类的成员函数可以访问此类包括非公有成员在内的所有成员。friend class A;

友元关系不存在传递性

尽管重载函数的名字相同,但它们仍然是不同的函数。因此,如果一个类想把一组重载函数声明成它的友元,它需要对这组函数中的每一个分别声明。

一个类就是一个作用域。

如果成员是const、引用,或者属于某种未提供默认构造函数的类类型,我们必须通过构造函数初始值列表为这些成员提供初值。

初始化和赋值的区别事关底层效率问题:前者直接初始化数据成员,后者则先初始化再赋值。

成员的初始化顺序与它们在类定义中的出现顺序一致,与构造函数初始值列表中初始值的前后位置关系无关。

对于C++的新手程序员来说有一种常犯的错误,它们试图以如下的形式声明一个用默认构造函数初始化的对象:

1
2
Sales_data obj1(); //定义了一个函数而非对象
Sales_data obj2; //用默认构造函数定义了一个对象

隐式的类类型转换:可以用单个实参来调用的构造函数定义了从形参类型到该类类型的一个转换。

1
2
3
4
5
6
7
8
9
10
11
class A {
public:
    A(int n) {};
    void add(A a) {};
};

int main() {
    int num = 1;
    A a1(1);
    a1.add(num); // 等价于先用num构造一个A对象,再传入a1.add
}

explicit关键字可以抑制构造函数定义的隐式转换。将上述构造函数改为explicit A(int n){};,则会编译报错:error: no matching function for call to ‘A::add(int&)’

类的静态成员:在成员的声明之前加上关键字static使得其与类关联在一起,而与每个对象无关。

因为静态数据成员不属于类的任何一个对象,所以它们并不是在创建类的对象时被定义的。这意味着它们不是由类的构造函数初始化的。而且一般来说,我们不能在类的内部初始化静态成员。相反的,必须在类的外部定义和初始化每个静态成员。和其他对象一样,一个静态数据成员只能定义一次。

类似于全局变量,静态数据成员定义在任何函数之外。因此一旦它被定义,就将一直存在于程序的整个生命周期中。

通常情况下,类的静态成员不应该在类的内部初始化。然而,我们可以为静态成员提供const整数类型的类内初始值,不过要求静态成员必须是字面值常量类型的constexpr

1
2
3
class A{
  static constexpr int period = 30;
}