C++Primer-C++基础

C++Primer阅读笔记

Posted by Honcy Ye on January 28, 2022

基本内置类型

算数类型

  • bool的最小尺寸是未定义的,根据具体平台的实现决定
  • wchar_t:类型用于确保可以存放机器最大扩展字符集中的任意一个字符
  • char16_t和char32_t则为Unicode(用于表示所有自然语言字符的标准)服务
  • long long 是C++11才有的,另外还有一个long double一般是3到4个字表示,但是很少用,一般被用于有特殊浮点需求的硬件
  • 字节:可寻址的最小内存块;存储的基本单位是字,一般是4或者8个字节
  • 类型:决定了数据所占的比特数和该如何解释这些比特的内容
  • 一般float和double分别有7和16个有效位
  • char有三种,signed char和char并不一样,所以经理不要用char参与算数运算,实在要用就用unsigned或者signed char
  • 当我们赋给一个有符号类型一个超出它表示范围的值时,结果是未定义的
  • 当一个算数表达式中既有无符号数也有有符号数的时候,有符号数会被转成无符号的
  • 指定字面值的类型:L’a’-宽字符型字面值,u8”hi!”-utf-8字符串字面值,42ULL-unsigned long long

变量

  • 具名的、可供程序操作的存储空间
  • 每个变量都有其数据类型
  • C++中变量和对象同义词,可互换
  • 列表初始化:用花括号,特点是存在丢失新型的风险时,编译器会报错,比如int a{ld}; 其中ld是double型的
  • 类的对象如果没有显式地初始化,其值由类确定
  • 声明和定义区分开来的原因:支持分离式编译。声明使名字为程序所知,定义则负责创建与名字关联的实体
  • 对于变量,如果只想声明,就加一个extern并不初始化,只要有初始化,即使加了extern也是定义
  • 用户自定义的标识符中不能连续出现两个下划线,也不能以下划线紧连大写字符开头,且定义在函数体外的标识符不能以下划线开头
  • 空指针不指向任何一个对象,建议用nullptr来表示指针为空,而不用老式的NULL预处理变量值为0. nullptr是一种特殊的字面值,可以被转换为任意其他的指针类型。
  • 建议初始化所有指针,而且变量定义后再定义指向它的指针,实在不行就定义为nullptr。
  • void*:和别的指针比较、作为函数输入输出、赋值给另一个void *。但是不能直接操作其所指的对象。
  • int *p:依次为基本数据类型、类型修饰符和变量标识符
  • int *&r = pr表示对指针p的引用,离变量名最近的符号对变量的类型有直接影响,其余部分用以确定r引用的类型是什么。指针或者引用的声明语句,从右往左读。
  • const变量必须初始化,不过可以是字面值这种编译时初始化,也可以是函数返回值这种运行时初始化
  • 默认状态下,const对象仅在文件内有效。如果想要在多个文件之间共享const对象(通常发送在其初值部署一个常量表达式的时候),必须在变量的定义之前添加extern
  • 对const常量的引用,在定义时也要加const,当然,变量也可以定义const引用
  • 指向const的指针也是一样,其指向的可以是变量,只不过不同通过该指针去改变变量的值罢了
  • const指针:指针本身是常量,int *const p = &a;
  • 顶层const:表示指针本身是常量;底层const:表示指针指向的对象是一个常量(不能通过指针改变它)
  • constexpr和常量表达式:常量表达式指值不会改变且在编译过程就能得到计算结果的表达式,比如字面值,用常量表达式初始化的const对象(这里就要区分两种const对象了)。比如const int sz = get_size();sz就不是常量表达式
  • 为什么要有constexpr变量?一个复杂系统,几乎不能分别一个初始值是不是常量表达式。C++11允许将变量声明为constexpr类型以便让编译器验证变量的值是否是一个常量表达式。
  • 不能使用普通函数作为constexpr变量的初始值,但是新标准允许定义一种constexpr函数,这种函数必须足够简单以使得编译时就可以计算其结果
  • 一般来说,如果认定一个变量是一个常量表达式,就把它声明为constexpr类型。因为常量表达式的值需要在编译时就确定,所以对声明constexpr时用到的类型必须有所限制,只能是算数、引用和指针这种字面值类型,不能是自定义类型。constexpr指针的初值必须是nullptr、0或者是存在某个固定地址的对象的地址(全局变量). constexpr引用同理。
  • 指针、常量和类型别名:pstring表示的是cstr的指针变量,而这个指针变量是const,因此cstr就是常量指针,而非指向常量的指针。这里容易错的是,把char*直接替换,这样会得到指向常量的指针的结果,因为替换后 *从本来的基本数据类型的组成部分,变成了声明符的一部分。
typedef char *pstring;
const pstring cstr = 0;
  • auto sz = 0, pi = 3.14因为sz和pi的数据类型不一致,auto无法推断
  • auto会忽略顶层const ,同时底层const则会被保留下来
  • 如果希望推断出的auto类型是一个顶层const则需要明确指出const auto f = ci; ci是int
  • decltype类型指示符:用于从表达式推断出要定义的变量类型,但是不想用该表达式的值初始化变量。它处理顶层const和引用的方式与auto有些不同。如果处理的是变量,则返回该变量的类型(包括顶层const和引用在内)
  • 如果decltype作用的是一个表达式,而且内容是解引用操作,那么返回的也是引用类型,如果decltype(()),有两层括号,那么结果永远是应用类型。如果r的引用,想让它返回其所指的类型,那就可以把r作为表达式的一部分,比如r+0.
  • C++11开始可以用类内初始值,它或者放在花括号里,或者放在等号右边,不能用圆括号。
  • 类一般定义在头文件中,头文件通常包含那些只能被定义一次的实体,如类、const和constexpr变量
  • 头文件中通常又会包含其他头文件,为了防止重复包含,要用预处理器的文件保护符功能,#define用来定义预处理变量。预处理变量不受C++的作用域规则的限制。
  • 头文件中不应包含using声明,防止不经意间包含了一些名字造成冲突。

字符串、向量和数组

直接初始化和拷贝初始化

  • 直接初始化:string s5("hiya");
  • 拷贝初始化:string s6 = "hiya";

string对象的操作

  • getline(cin, line);line为string对象,表示从标准输入读一行,包括换行符,不过换行符不会存入line中
  • string::size_type类型是一个无符号整数,每个标准库的类型都包含了几种配套的类型以体现与机器无关的特性。表达式中如果用了它,就不要用int了,除非把它转成int。
  • 不能把字符串字面值直接相加,至少要包含一个string的对象,因为C++为了与C兼容,字符串字面值并不是string对象

vector

  • 可以把模板看作 为编译器生成类或者函数所编写的一份说明。而编译器根据模板创建具体的类或者函数的过程就称为实例化。
  • 初始化:vector v={a, b, c,...}等价于vector v{a, b, c,...}列表初始化
  • 值初始化:指的是初始化vector对象时,只提供了元素数量,没有初值,这时就会创建一个值初始化的元素初值,内置类型就是默认值,类类型就是默认初始化且无论该vector对象是全局还是局部对象(值初始化只是一种实际使用过程中的初始化场景,而非某种初始化类型)。但这种方式不适用于所有场景。比如有的类要求必须明确地提供初值;或者对象中元素的类型不支持默认初始化的情况。
  • {}花括号的形式非常灵活,编译器会优先考虑列表初始化对象,但是如果用了花括号的形式但是里面的值又不能用来列表初始化时,就会考虑用里面的值来构造对象,即考虑用其他初始化。比如vector<string> v{10, "hi"};

迭代器

  • begin()和end()返回的具体类型由对象是否是常量决定,如果对象是常量,则返回const_iterator否则返回iterator。另外还有cbegin()和cend()来直接得到const_iterator
  • 只有string 和vector支持迭代器运算,就是加减整数和大小比较,其他的STL容器不支持。difference_type则是一种带符号整数类型,用来表示两个迭代器间的距离。

数组

  • 数组初始化用列表初始化的两种形式
  • 复杂的数组声明,数据指针,指针数组,默认情况下都是从右往左依次绑定:
    • int *ptrs[10];这里记住一点,[]的优先级高于*和&。因此,前面这个首先是一个数组,它的元素是指针。
    • int &refs[10];这是错误的,因为不存在引用的数组
    • int (*parray)[10] = &arr; 这是一个指针,它指向一个包含10个元素的数组
    • int (&arrRef)[10] = arr; 这是一个引用,它指向一个含有10个元素的数组
  • 数组下标是一个无符号整数,使用里的size_t机器无关类型,防止下标越界是由程序员负责的。大多数常见的安全问题都来自于缓冲区溢出。
  • 大多数情况下包括auto,编译器都会自动把用到数组名字的地方替换为一个指向数组首元素的指针。但是decltype是例外,它得到的是数组。
  • 指针也是迭代器:大多数都相同,不过数组不是对象,所以没有成员函数,也就是无法直接调用end()得到尾后指针,要得到尾后指针,要不就是通过计算得到int *e = &arr[10],要不然就是C++11提供的标准库函数头中的end()和begin()。两指针相减的类型是ptrdiff_t的有符号整数类型。
  • 指针也可以有下标,而且可以是负数,这与vector、string不同。int *p = a[2];则p[[-2]就是a[0]。

C风格字符串

  • 字符串字面值即C风格字符串是一种通用结构的实例,它不是一种类型,在C++中最好不使用。即以’\0’结尾的字符串,存在字符数组中。
  • 任何出现字符串字面值的地方都可以用以空字符结束的字符数组来替代,但是反过来就不成立了。
  • 另外还可以用数组来初始化vector对象, vector<int> ivec(begin(int_arr), end(int_arr)); vector<int> ivec(int_arr + 1, int_arr + 3);
  • 使用范围for语句处理多维数组时,如果要修改数组元素,则除了最内层循环外,其他所有循环的控制变量都应该是引用类型。因为,否则的话,数组会被自动转成指针而报错。
  • 可以用auto、decltype和类型别名using int_arr = int[4];它等价于typedef int int_arr[4]

表达式

  • 表达式不是左值就是右值,当一个对象呗用作右值的时候,用的是对象的值(内容);当对象被用作左值的时候,用的是对象的身份(在内存中的位置)。不同运算符对对象的要求不同,有的要求左值有的要求右值。
  • C++11规定商一律向0取整
  • m%n = m - (m/n)*n; m%(-n) = m%n; (-m)%n = -(m%n)
  • 没必要就不用后置++/–,因为背后编译器需要将原始值暂存起来,这对 迭代器这种复杂类型来说开销较大
  • *pbegin++, ++的优先级高于 *,所以是先输出当前指向的值,再自增
  • sizeof (type)或者sizeof expr两种。第二种返回的是结果类型的大小,但是它实际并不计算表达式的值。sizeof *p 由于两个运算符的优先级相同,所以是先结合 *。
    • 对vector和string对象执行sizeof只返回该类型固定部分的大小。
    • sizeof返回一个常量表达式,可以用来 声明数组维度

类型转换

  • 整型提升:对于bool、char、unsigned char、short、unsigned short这些类型,只要它们的值能存在int里,都首先提升为int,否则提升为unsigned int

  • 如果无符号类型的所有值都能存在带符号类型中,则无符号的转成带符号的,否则相反。

  • static_cast<>:用于任何除了底层const之外的类型转换

    • void*指针-> 其他类型指针,这个编译器无法自动执行
    • 明确程序员知道会发生精度损失,传统的强转这里编译器就会给警告
  • const_cast<>:去掉底层const。原因:我们可能调用了一个参数不是const的函数,而我们要传进去的实际参数确实const的,但是我们知道这个函数是不会对参数做修改的。于是我们就需要使用const_cast去除const限定,以便函数能够接受这个实际参数。

    const int constant = 21;
    const int* const_p = &constant;
    int* modifier = const_cast<int*>(const_p);
    *modifier = 7;
    
  • reinterpret_ cast对应C语言中的强制转换。C 语言中 能隐式类型转换的,在 C++中可用 static_cast<>()进行类型转换。因 C ++ 编译器在编译检查一般都能通过; C 语言中不能隐式类型转换的,在 C ++中可以用 reinterpret_cast<>() 进行强行类型

异常处理

异常处理包括:

  • throw表达式:异常检测部分使用throw表达式来表示它遇到了异常,即throw引发了异常。表达式的类型就是抛出的异常类型, throw(runtime_error(“……..”))
  • try语句块:以一个或者多个catch子句结束,catch子句也称为异常处理代码。catch(runtime_error err)
  • 一套异常类:用于在throw表达式和相关的catch子句之间传递异常的具体信息。

  • 编写异常安全的代码非常困难

函数

含有可变形参的函数

  • 有时无法提前预知应该向函数传几个实参,比如要输出程序产生的错误信息,这时最好用同一个函数实现。
  • 方法一:适用于实参类型相同的情况,使用initializer_list
  • 方法二:适用于实参类型不同的情况,编写一种特殊函数,即可变参数模板
  • C里面的省略符… 它对应的实参无需类型检查

返回值

  • 不要返回局部对象的引用,函数外部的对象是可以的
  • 列表初始化返回值:return {…, …}; 用来对函数返回的临时量进行初始化
  • main函数的返回值可以用里的预处理变量EXIT_FAILURE和EXIT_SUCCESS
  • 返回数组指针
    • 使用类型别名:typedef int arrT[10]; using arrT = int[10]; arrT *func();
    • int (*func(int i))[10];
    • C++11加入的后置返回类型:auto func(int i) -> int(*)[];
    • decltype(odd) *arrPtr(int i);odd是数组

函数重载

  • main函数不能重载

  • 一个形参有无顶层const无法区分

  • const_cast和重载

    const string &shorterString(const string &s1, const string &s2)
    {
        return s1.size() <= s2.size() ? s1: s2;
    }
    // 期望传输非常量引用时,返回的也是非常量的引用,因为返回值不是函数签名,所以常规方式无法区分这两种,这就能用const_cast解决
    string &shorterString(string &s1, string &s2)
    {
        auto &r = shorterString(const_cast<const string &>(s1), const_cast<const string&>(s2);
        return const_cast<string&>(r);
    }
    
  • constexpr函数:返回类型和所有形参都得是字面值类型,且函数体中必须且只能有一条return语句。会被隐式的指定为内联函数,且会在编译时替换为结果值。函数中可以有其他语句,只要语句在运行时不执行任何操作即可

  • 内联函数和constexpr函数可以有多个定义,但是必须一致,所以一般把它们放在头文件里。

调试帮助

  • 开发的时候写在代码里,发布的时候屏蔽掉
  • assert预处理宏,即一个预处理变量,要包含头文件,但无需指定命名空间。assert(expr)值为false时会终止程序并报错。否则什么都不做。
  • NDEBUG预处理变量:assert的行为依赖于该变量。如果定义了NDEBUG则什么也不做。除了#define可以定义它,很多编译器提供了编译命令行选项可以定义它。即 -D NDEBUG
  • 定义NDEBUG可以避免检查各种条件所需的运行时开销,当然此时并不会执行运行时检查
  • 除了用assert也可以使用NDEBUG编写自己的条件调试代码。即用#ifndef NDEBUG … #endif
  • cerr << __func__ << ": array size is " << size << endl;
    • func:C++编译器定义的一个局部静态变量,用于存放函数的名字,是const char数组
    • FILE:存放文件名的字符串字面值
    • LINE:存放当前行号的整型字面值
    • TIME:存放文件编译时间的字符串字面值
    • DATE:存放文件编译日期的字符串字面值。这四个都是预处理器变量

  • 类的基本思想:数据抽象和封装。其中封装是实现类的接口和实现的分离。而数据抽象则代表依赖于封装的编程(设计)技术。
    • 类要实现数据抽象和封装,首先要定义抽象数据类型,也就是体现封装思想的数据类型:使用该类的程序员只需要抽象地思考类型做了什么,无需关注实现。
    • 具体讲,如果定义的类提供了public成员变量就不是抽象数据类型
  • 有时候我们除了定义类的成员函数,还要定义一些类相关的非成员函数,用来操作类对象,这类函数通常和类声明在同一个头文件中。

构造函数

  • 无论何时,只要类的对象被创建,就会执行构造函数。
  • 构造函数不能被声明为const
  • 合成的默认构造函数:
    • 如果有类内初始值就用它初始化
    • 否则,默认初始化(和数据类型及是否在块中有关)
    • 有时候在提供了类内初始值的情况下,想要编译器给合成默认构造函数,但是同时又需要自己定义其他构造函数,这时候就需要在类的声明中声明一个默认构造函数并加上= default。

拷贝、赋值和析构函数

  • 编译器也会在我们没有自定义这些函数的时候为我们合成它们。但是某些类不能依赖于编译器,需要我们自己定义这些函数。
  • 比如,当类需要分配类对象之外的资源时,比如管理动态内存的类,就不能依赖编译器合成。
  • 使用vector或者string的类可以避免分配和释放内存带来的复杂性。如果类包含vector或者string成员,则其拷贝、赋值和析构的合成版本能正常工作。

友元

类相关的非成员函数,用来操作类对象,可能要访问类的成员变量,这时候就需要在类声明里将这些函数声明为友元。

类的其他特性

  • 类型成员:定义在类内定义typedef/using的类型别名可以隐藏类的实现细节。
    • 比如一个Screen类,表示显示器的一个窗口,string成员存显示的内容,三个string::size_type成员存屏幕长宽和cursor光标位置
    • 为了隐藏Screen的实现细节,就可以把string::size_type类型别名为pos,这样用户就不知道是用string来实现的
    • 类型成员也有访问限制,而且只有前面定义的后面才能用,和类成员不同。
  • 定义在类内的成员函数自动是inline的,类外也可以定义inline类成员函数
  • 成员函数也可以重载
  • 可变数据成员mutable:有时候,需要类内的某些成员永远是const,比如,access_ctr用来追踪Screen类成员函数被调用的次数。这时,对于某个const成员函数,它并不修改成员变量,但是需要更新access_ctr以做统计。mutable可变成员就是为解决这种场景而设置的。
  • 返回*this的成员函数:为什么要有这种形式?直接返回void不行吗?因为直接返回void的不能支持myscreen.display(cout).set('*')这种连续调用同一个对象的不同成员函数的情况。
  • 一个const成员函数如果以引用的形式返回 *this,那么它的返回类型将是常量引用。这对于myscreen.display(cout).set('*')这种连续调用的情况会报错
  • 通过区分成员函数是否是const的,可以对其进行重载。当通过某个对象调用重载的函数时,对象是const,则调用的就是该函数const的版本,否则就是非const的版本。
  • 很多时候(就比如前面的const重载),需要给某些函数定义执行实际工作的函数do_*(),比如前面的display()和do_display()。这样做的好处在于:
    • 避免多处代码重复,两个重载函数,只有const的不同,那代码就没必要写两份,特别是display函数很复杂的时候
    • 打开关闭调试信息这种,如果不统一放到do_display函数里,就要在多个display()的重载函数中改多次
    • 性能方面:do_*()函数是类内定义的inline函数,并不会引入额外开销
  • 类的声明:class Screen,前向声明,在声明之后,定义之前,它只是不完全类型。这里注意,类的声明和定义与类成员函数的声明和定义是分开的。
    • 不完全类型的适用场景:可以定义指向这种类型的指针或引用,声明但不能定义以不完全类型作为参数或者返回类型的函数。
    • 类的数据成员不能是它自己,但是可以是它自身类型的引用或者指针。
  • 友元还可以把其他类定义成友元,或者其他类的成员函数定义为友元,且每个类负责控制自己的友元类或者友元函数,不能传递。

类的作用域

  • 每个类都会定义它自己的作用域,所以类外定义成员函数要加作用域运算符
  • 编译器处理完类中的全部声明(整个类可见)后才会去处理成员函数的定义。

  • 成员的初始化顺序与它们在类定义中的出现顺序一致。和它们在初始化列表中的位置无关。
  • 如果一个构造函数为所有参数都提供了一个非零的值,则它实际上也定义了默认构造函数。

  • 隐式的类类型转换:如果构造函数只接收一个实参,则它实际上定义了转换为此类类型的隐式转换规则。这种构造函数也叫转换构造函数。比如Sale_data类有一个接收一个string参数的构造函数,那么在使用Sale_data对象的地方也可以使用string对象作为替代。
  • 但是这个需要注意的是,只允许一步类型转换,即如果不是传入string对象而是传入一个字符串字面值的话就不行
  • 如果要拒绝这种隐式转换,则可以在相应构造函数前面加explicit关键字
  • 聚合类就是C语言里的struct
  • 字面值常量类:数据成员都是字面值类型的聚合类;还有就是至少含有一个constexpr构造函数,如果成员含有类内初值,则其初值必须是一条常量表达式