C++17入门经典

C++17入门经典

  • 注意
  • 第1章 基本概念
  • 第2章 基本数据类型
  • 第3章 处理基本数据类型
  • 第4章 决策
  • 第5章 数组和循环
  • 第6章 指针和引用
  • 第7章 操作字符串
  • 第8章 定义函数
  • 第9章 函数模板
  • 第10章 程序文件和预处理指令
  • 第11章 定义自己的数据类型
  • 第12章 运算符重载
  • 第13章 继承
  • 第14章 多态性
  • 第15章 运行时错误和异常
  • 第16章 类模板
  • 第17章 移动语义
  • 第18章 头等函数
  • 第19章 容器与算法
  • 第20章 STL
    • 20.1 容器(Containers)
    • 算法(Algorithms)
    • 迭代器(iterators)
    • Eigen

注意

  • 任何数字,只要其分数部分是奇数,就不能准确地表示为二进制浮点数。

第1章 基本概念

  • C++适合写哪些程序?
    答:C++几乎可以写任何程序,最适合编写对性能有较高要求的应用程序,如大数据处理、大型游戏、嵌入式或移动设备上的程序等。
  • 什么是现代C++编程?
    答:使用C++11及更新版本的功能进行编程称为“现代C++”编程
  • C++标准版本的发布间隔?
    答:3年
  • 源文件和头文件及其扩展名
    答:源文件包含函数和全部可执行代码。.cpp、.cc、.cxx、.c++
           头文件包含源文件中的可执行代码使用的函数原型,以及使用的类和模板的定义。.h、.hpp
  • 如何注释单行或多行内容?
    答:单行//      多行/*…*/
  • 预处理指令及其作用,如何包含头文件?
    答:以某种方式修改源代码,之后把它们编译为可执行的形式。#include 把iostream标准库文件的内容添加到其所在文件中的#include位置
  • 什么是函数?main函数的特点?什么是函数头?
    答:1)函数为一个命名了的代码块,执行定义好的操作;2)程序的执行从main函数开始;3)函数由函数头和花括号中的可执行代码2部分组成,函数头用于标识函数,如int greater(intn a, int b)定义了main函数的返回类型、函数名、输入参数。
  • 变量
    答:变量是一个命名了的内存块,用于存储某种数据。
  • 语句的地位、语句块、复合语句,什么是嵌套?
    答:1)语句是C++的基本单元,总以分号结束;2)花括号中的多个语句就是语句块,又称复合语句;3)将一个语句块包含在另一个语句块内的概念叫作嵌套。
  • 如何执行数据的输入输出?
    答:使用“流”来执行输入输出,流是数据源或数据接收器的一种抽象表示。
  • return语句的作用,不同返回值的含义
    答:return用于结束函数,把控制权返回给调用函数的地方。return语句可能返回一个值,也可能没有返回值。只有main()函数中,忽略return语句才相当于返回0。
  • 名称空间的作用
    答:用于解决名称定义的混乱
  • 名称的定义规则、保留字的特征
    答:1)可用字母、数字、下划线;必须以下划线或字母开头;区分大小写。2)关键字属于保留字,许多保留字以下划线开关,因此避免使用下划线开关的名称定义。

第2章 基本数据类型

  • 变量初始化的3种形式:初始化列表、函数表示法、赋值表示法,建议用哪一种,原因
    答:
    初始化列表初始化:int apple_count {15};
    函数表示法:int orange_count (5);
    赋值表示法:int total_fruit = apple_count + orange_count;
    建议使用初始化列表进行初始化,可更好避免缩窄转换(float ⇒ \Rightarrow int)。

  • 为什么最好在单个语句中定义每个变量?
    答:指不用int a{1}, b{2}, {3};形式。因为在单个语句中定义每个变量可提高代码的可读性。

  • 什么是字面量?说明各种字面量的类型
    答:指各种类型的常量。每个字面量都有特定的类型。

    常量 字面量 类型(const) 语句
    -123 整型字面量 int int value{-123};
    123’456’789LL 整型字面量 long long long distance {123'456'789LL};
    123u 整型字面量 unsighed short unsighed short price {123u};
    2.3245 浮点型字面量 float float factor {2.3245};
    ‘Z’ 字符字面量 char char alph {'Z'};
    “number” 字符串字面量 char [] char name[] {"Mae West"};
    ture 布尔字面量 bool
  • 为什么建议仅在有充足理由时使用using指令?
    答:使用using指令声明名称空间后,using std::cout;,就不需要使用名称空间限定名称了,这种用法过多会增加名称冲突的可能性。

  • sizeof运算符及其结果类型
    答:可对类型、变量、表达式使用,得到其所占用的字节数,结果类型为size_t;
           size_t不是内置的基本类型名称,而是标准库定义的一个类型别名;
           size_t是一个不带符号的整数,可存储任何类型理论上可能存在的对象的最大大小,常用于数组索引和循环计数。

  • 递增递减运算符的前后缀形式的区别
    答:前缀先计算值再计算表达式,后缀先计算表达式后计算值

  • 浮点类型什么时候用double,什么时候用float?
    答:大多数情况下,使用double类型就足够了,只有速度或数据大小非常关键时,才会使用float。

  • 数学函数头文件
    答:cmath头文件定义了许多三角函数和数值函数,所有函数名都在std名称空间中定义。函数的结果总是与浮点型参数的类型相同,整型参数的结果为double类型。

  • 输出流格式化头文件(2个)及其区别
    答:iostream头文件,无参数
           iomanip头文件,有参数

  • 什么是显式类型转换,为什么需要它?如何实现?
    答:把表达式的值显式转换为给定类型static_cast (表达式),显式强制转换以得到希望的结果类型。static_cast表示进行该强制转换要进行静态检查。

  • 如何确定数值的上下限?
    答:std::numeric_limits::max()std::numeric_limits::min()

  • 字符变量(char类型)是否可参与算术表达式?
    答:可以,因为char类型的变量是数值,它们存储了表示字符的整数代码,所以它们可以参与算术表达式。

  • auto关键字的使用场景,是否推荐用于定义基本类型变量?为什么?
    答:用于推断类型,建议只用于推断很长的自定义类型、长的指针等,定义基本类型的变量应显式指定类型。


第3章 处理基本数据类型

  • 运算符的执行顺序和什么有关?
    答:执行顺序由运算符的优先权决定。

  • 枚举类型的定义及使用枚举类型定义变量的语句实现
    答:enum class Day {Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday};
           Day today {Day::Tuesday};
           要输出today的值,就必须先把它转换为数值类型,因为标准输出流不能识别自定义类型std::cout << "Today is " << static_cast (today) << std::endl;}

  • 新旧枚举类型定义方法的不同及新方法的优势
    答:旧语法              enum Day {Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday};
           C++11后新语法enum class Day {Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday};
           新语法在定义时加入class关键字,使得枚举成员在转换为整型甚至浮点类型时,会进行强制转换,更不容易出错。

  • 枚举类型成员的默认值及成员显式赋值
    答:1)第一个枚举成员的值默认为0,其后的成员依次加1;
           2)枚举成员不一定有唯一值,且不一定必须以升序进行赋值;
                 可以根据以前的枚举成员定义新的枚举成员值。enum class Day {Sunday=7, Monday=1, Tuesday=Monday, Wednesday, Thursday, Friday, Saturday};

  • 枚举成员值类型的要求
    答:枚举成员的值必须是编译期间的常量,即编译器可以计算出来的常量表达式,只包括字面量、以前定义的枚举成员、声明为const的变量;
           枚举成员可以是包含默认类型int在内的任何整数类型

  • 枚举成员的类型规范
    答:枚举成员的类型规范放在枚举类型名的后面,用冒号隔开enum class Punctuation : char {Comma = ',', Exclamtion = '!', Question = '?'};

  • 数据类型的别名的新旧指定方式及新方法对比旧方法的优势
    答:旧方式typedef unsigned long long BigOnes;
           新方式using BigOnes = unsigned long long;
           新方式更加直观,可读性也更好,但写出具体类型能够让代码更容易理解,因此应该有节制的使用类型别名。

  • 变量的生存期(4种)
    答:变量生存多长时间取决于其存储持续时间

    生成方式 存储持续时间 称谓 解释
    在代码块中声明的非静态变量 动态的存储持续时间 自动变量、局部变量 从声明它的那一刻开始,到包含其声明的代码块的结尾结束,具有局部作用域或块作用域
    使用static关键字定义的变量 静态的存储持续时间 静态变量 从定义的那一刻开始,到程序结束时消失
    在运行期间分配内存的变量 动态的存储持续时间 从创建它们那一刻开始,到释放其内存、销毁它们时消失
    使用thread_local关键字声明的变量 线程存储持续时间
  • 全局变量的特点及其访问方式、优缺点
    答:全局变量在所有代码块和类外部定义,具有 全局(名称空间)作用域;
           在默认情况下具有静态的存储持续时间;
           初始化在main()之前进行,若没有初始化,则默认初始化为0(自动变量在没有初始化时包含的是垃圾值)
           要访问全局变量value,必须使用作用域解析运算符::限定它std::cout << "Global value = " << ::value << std::endl;
           全局变量使用过多会占用较多内存,且会大大增加修改变量时出错的可能性,因此原则要求避免使用全局变量,但是很适合用于定义全局常量,即用const声明的全局变量。


第4章 决策

  • 布尔类型
    答:条件为 bool 类型,bool 值只有 true 和 false,它们是 bool 类型的字面量,布尔字面量;
           bool 类型若使用空{}来初始化,初始值为false;
           在条件语句中使用关系运动符和逻辑运算符(&&, ||, !)可完成各种复杂的条件;
           某类型中只有0的对等值会被转换为布尔值false

  • bool值的输出
    答:bool默认显示为0或1,可以使用std::boolalpha将其显示为true或false

    std::cout << std::boolalpha;
    std::cout << (5>3) <<std::endl;
    
  • if语句及其嵌套
    答:

    if (condition){
    }
    else{
    }
    
  • 条件运算符
    答:若条件为真,则c=表达式a,否则c=表达式b c = 条件? 表达式a : 表达式b;

  • switch语句及其类型要求
    答: switch的选择表达式应为整数表达式,即整型、字符型、枚举类型等;
           case值必须唯一,但不必按顺序;
           选择表达式对应哪个标签值,就执行哪个case后的语句;
           若选择表达式不对应任一标签值,就执行default标签后的语句;
           一般每个case后的break语句都是必需的;
           default标签后的break语句不是必需的,但加上它是个好习惯。

    switch (选择表达式){
    case 标签1:
        ...
        break;
    case 标签2:
        ...
        break;
    case 标签n:
        ...
        break;
    default:
        ...
        break;
    }
    
  • switch语句的“贯穿”现象
    答:当移除了某个case后的break语句时,它下面的case标签的代码也会运行,这叫作贯穿;
           C++17为故意使用贯穿添加了新的语言功能,将原本的break语句替换为[[fallthrough]];

  • switch中case标签后的多条语句什么时候需要加花括号
    答:1)case后的多条语句一般不需要加花括号;
           2)因为花括号内的代码构成代码块,是自动变量或局部变量的生存范围。因此当需要使用局部变量时,就需要加上花括号。见下一条。

  • switch语句块中各个位置的变量的作用域
    答:switch语句内的变量是自动变量;
           其中自动变量的定义要能保证在正常执行过程中可以被访问;
           因为是自动变量,所以从它的定义到整个switch语句的结束都是它的作用域,不能绕过变量的定义而进入其作用域,因此,case中若要进行变量的定义,就要加花括号,形成这个case自己的语句块;
           对于放在最后的case,比如default,由于其后没有其他case,所以肯定不能绕过,因此可以进行变量的定义而不加花括号。

  • C++17中,为if语句和switch语句添加了初始化语句,以将变量限制到if或switch语句块:

    if(initialization; condition) {...}
    switch(initalization; condition) {...}
    

第5章 数组和循环

  • 数组
    答:数组可以存储相同任意类型的多个数据项;
           数组元素占用的内存量由数组类型决定;
           数组的所有元素都存储在一个连续的内存块中;
           数组的大小必须用常量表达式来指定;
           初始化列表的个数不能超过数组的元素个数;
           初始化列表中未指定值的元素默认初始化为0;
           编译器不会检查数组索引值是否有效,程序员应自己确保引用的元素不会超出数组边界。
           usigned int height[6] {24,34,73};
  • 幻数,如何避免,为什么要避免
    答:幻数是裸字面常量,由于没有名称,无法看出它的含意。
           使用幻数会导致代码可读性差,因此,应该定义幻数或任何常量且仅定义一次
  • for循环
    答:控制for循环的任一表达式或所有表达式都可以省略,但分号必须有。
           for(初始化;条件;迭代) {循环体}
  • 使用std::size(array)来确定数组的大小,控制for循环的迭代次数
    答:std::size()可用来获得标准库定义的任何元素集合的大小,该函数头文件为。注意该函数为C++17引入,所以需设置C++标准
           int value[] {1,2,3,1,2,34,23,4,5,234,23,22};
           for (size_t i {}; i
  • 基于范围的for循环,无符号整数控制for循环
    答:什么是范围:标准库提供的容器都是范围
           for (range_declaration : range_expression)
                  loop statement or block;
           range_expression为数据源的范围,range_declaration标识一个变量,它会被依次赋予范围中的每个值,在每次迭代都会赋予一个新值。
  • while循环,do-while循环
    答:while(条件) { }
           do{ }while(条件);
  • continue,break的使用
    答:continue;  立即跳到当前迭代的末尾,直接开始下一次迭代;
           break;       立即终止循环,开始执行循环后面的语句。
           应该谨慎使用break语句;
           应尽可能将决定循环何时结束的条件放到for或while语句的圆括号内。
  • 字符数组:char类型数组中,存储字符的数组与存储字符串的数组的区别
    答:char类型的数组有两种形式:<1>作为一个字符数组,每个元素存储一个字符;
                                                          <2>作为一个字符串,每个字符存储在一个数组元素中,结尾以 空字符’\0’ 表示。
           char类型中0的对等值为’\0’,故若以少于数组元素个数的字符初始化数组,数组就会包含一个字符串;
           该数组有9个元素;char name[] {"Mae West"};
           对于以空字符’\0’结尾的char类型数组,可以使用数组名输出数组内容,其他类型不可以。std::cout << name << std::endl;
  • 如何读取输入的包含空格的字符串
    答:
    #include 
    const int max_length {100};
    char text[max_length] {};
    std::cin.getline(text, max_length, '所指定的标志输入结束的字符(默认为'\n')');
    
  • 多维数组
    答:多维数组最右边的索引值总是变化最快,最左边的索引值则变化最慢;
           编译器无法推断第一个维度之外的其他维度,必须总是显式指定除第一个维度外的全部数组维度;
           花括号的嵌套次数就是数组的维数。
           严格来说,C++中没有多维数组,所谓的多维数组其实是数组的数组
    double carrots[3][4] {	//定义了一个大小为3的数组,该数组的每个元素都是含有4个double类型数值的数组,数组名与起始地址为carrots
    					  {2.5, 3.2 },
    					  {4.3 },
    					  {5.3, 2.5, 6.4 }
    					 };
    int ia[3][4] = {0,1,2,3,4,5,6,7,8,9,10,11};	//内部每行的花括号是可选的
    
  • 多维字符数组的定义
    答:char类型的二维数组可以存储一组C样式的字符串;
           使用字符串字面量初始化char类型的二维数组时,界定字符串的双引号就已经完成了花括号的工作。
    char stars[][80] {
    					"Robert Redford",
    					"Hopalong Cassidy",
    					"Lassie",
    				   };
    
  • std::array容器,头文件,定义,初始值设置,访问,
    #include 
    std::array<double, 100> values;
    //std::array values {0.3, 0.45, 0.23, 0.5};
    values.fill(0.5);
    double total {};
    for (size_t i {}; i<= values.size(); ++i) {
        total += values.at(i);					//使用at()访问array<>对象的元素,会自动检查索引值的有效性
    }
    value[4] = value[3] + 2.0*values[2];		//使用[]不会进行检查
    
  • std::vector容器,头文件,定义,初始值,添加元素,
    #include 
    std::vector<double> values;
    values.push_back(0.5);
    std::vector<double> values1 (20);					//20个元素被初始化为0
    std::vector<long> values2 (20, 99L);				//20个元素被初始化为99L
    std::vector<unsigned int> values3 {1, 2, 3, 4, 5}; 	//使用初始化列表进行初始化
    

第6章 指针和引用

  • 语句long* pnumber {}; 中,变量pnumber的类型写作long* ,读作___ ?
    答:指向long
  • 对应于0的空指针为___ ?
    答:nullptr
  • 定义指针时,是否需要初始化
    答:不是必须的,但总要初始化,至少将其初始化为nullptr
  • 地址运算符,间接运算符、解引用运算符
    答:地址运算符&,获取变量的地址
           间接运算符(解引用运算符)*,访问指针所指向的内存位置的数据
  • 指向char类型的指针的特殊初始化方式
    答:用字符串字面量初始化const char* pproverb {"A miss is as good as a mile."};
  • 指针数组相对于字符数组为什么占用的内存空间更小了
    答:每个字符串都占用容纳其所有字符所必需的字节数,而不是统一的长度;
           在字符串长度差异大或字符串较多时,用于存储字符串地址的内存相比上一条节省的内存是相当可观的
    const char* pstars[] {
    					  "Robert Redford",
    			  		  "Hopalong Cassidy",
    					  "Lassie",
    				     };
    
  • 常量指针、指向常量的指针、指向常量的常量指针
    答:
    类型 注释 示例
    常量指针 指针变量中的地址不能修改 int data {20}; int* const pdata {&data};
    (const修饰指针,指针为常量,指向int)
    指向常量的指针 指针指向的内容不能修改 const int value {20}; const int* pvalue {&value};
    (指针指向const int)
    指向常量的常量指针 指针变量中的地址与指针指向的内容都不能修改 const float value {3.14}; const float* const pvalue {&value};
  • void* 指针
    答:void* 是一种特殊的指针类型,可用于存放任意对象的地址。
           用途:和其它类型的指针比较、作为函数的输入或输出、赋值给另一个void指针
           不能直接操作void
    指针所指的对象,因为不知道所指对象的类型,只知道其起始地址
  • 为什么可以用数组名初始化指针
    答:数组名相当于一个在其定义时就固定了的地址
    double values[] {12.234, 28.243, 32.3, 4.54, 545, 6.445};
    double* pvalue {values};
    *(pvalue +1) = *(pvalue +2);
    
  • 数组名的指针表示法
    答:可以这样做的原因是编译器自动地将数组名替换为一个指向数组首元素的指针
    double data[] {};
    for (size_t i {}; i< std::size(data); ++i) {
        *(data + i) = 2 * (i+1);
    }
    
  • 指针数组与数组指针
    答:指针数组,元素均为指针类型数据的数组。
                             int *p[4];
                             []*优先级更高,说明p为一个数组,其中元素为int*(指向int)类型
           数组指针,指向数组的一个指针。
                             int (*p)[4]; //n是一行里有几个元素,也就是列数
                             ()优先级高,首先说明p是一个指针,指向一个整型的一维数组,这个一维数组的长度是n,也可以说是p的步长。也就是说执行p+1时,p要跨过n列的长度。
  • 动态内存分配
    答:在运行期间分配存储数据所需的内存空间(不是在编译程序时分配预定义的内存空间)
  • 自动变量、栈、堆、自由存储区
    答:自动变量      在执行其定义时创建的变量
           栈                 在内存区域给自动变量分配的空间
           自由存储区   操作系统和当前加载的其他程度未占用的内存
           堆                  一些人认为,C++中new和delete运算符操作的是自由存储区,C的内存管理函数操作的是堆
  • new、delete
    答:
    //分配内存
    1)	double* pvalue {};
    	pvalue = new double;		//new 返回新分配内存的地址
    2)	//double* pvalue {new double {3.14} };
    3)	double* data {new double[100] {} };	//对数组进行动态内存分配时,编译器无法推断数组的维数,因此应显式指定数组的大小
    //释放内存
    	delete pvalue;
    	pvalue = nullptr;	//释放内存后,原指针成为悬挂指针,应重新设置该指针或置为空指针
    	delete[] data;	//释放数组内存,要加上[],且这里不能填入维数
    	data = nullptr;
    
  • 多维数组的指针
    答:单从数组的指针的定义是看不出其所指向的数组的大小的,无论是指向一维还是多维数组
           由于多维数组其实是数组的数组,所以多维数组指针所指向的是内层数组的起始地址
    int ia[3][4] {0,  1,  2,  3,
    			  4,  5,  6,  7,
    			  8,  9,  10, 11};
    int (*p)[4] = ia;
    std::cout <<	p		 		<<std::endl;	//输出数组ia起始地址,	即ia[0][0]地址
    std::cout <<	*p		 		<<std::endl;	//输出数组ia[0]起始地址,	即ia[0][0]地址
    std::cout <<	*(p+1) 			<<std::endl;	//输出数组ia[1]起始地址,	即ia[1][0]地址
    std::cout <<	*(*(p+1) +2)	<<std::endl;	//输出数组ia[1][2]的内容
    
  • 多维数组的动态内存分配
    答:标准C++不支持有多个动态维数的多维数组,最多能让第一维的大小是动态的;(从多维数组指针的定义也能看出)
    size_t rows {3};
    //double (*carrots)[4] {new double [rows][4] {} };
    auto carrots {new double[rows][4] {} };				//C++11之后,可使用auto来代替上一句的写法
    ...
    delete[] carrots;
    carrots = nullptr;
    
  • 间接成员运算符(箭头运算符)
    答:通过类对象的指针访问其成员时,需要先对其解引用,再使用成员访问运算符.,这一过程可用间接成员运算符->代替
           std::vector data;
           auto* pdata = &data;
           //(*pdata).push_back(66);
           pdata->push_back(66);
  • 内存泄漏
    答:由于改写了指针中保存的地址,丢失已分配的自由存储区内存地址,而出现的问题
  • unique_ptr指针,定义,重置
    答:唯一地存储地址,独占拥有其所指向的值。
           常用于保存多态指针(指向动态分配对象的指针,允许动态分配的对象是任意数量的相关类类型)
    std::unique_ptr<double> pdata { std::make_unique<double>(999.0) };//make_unique为C++14引入
    auto pdata { std::make_unique<double>(999.0) };
    const size_t n {100};
    auto pvalues { std::make_unique<double[]> (n) };
    pvalues.reset();	//reset()可把任意类型的智能指针重置为指向nullptr.
    
  • shared_ptr指针,定义,初始化,赋值
    答:可以有任意多个shared_ptr对象包含或共享相同的地址,共享自由存储区中对象的所有权;
           包含给定地址的shared_ptr对象的个数,称为引用计数;
           引用计数自动维护,当为0时,释放占用的内存空间
    std::shared_ptr<double> pdata { std::make_shared<double>(999.0) };
    auto pdata { std::make_shared<double>(999.0) };
    pdata.reset();
    
  • vector容器、智能指针、数组的使用推荐
    答:优先使用vector,然后是智能指针,最后为数组
  • 语句double& rdata {data}; 中,变量rdata的类型写作double&,读作___ ?
    答:对double的引用
  • 为什么在基于范围的for循环中使用引用会很高效?
    答:使用引用避免了对象复制成本

第7章 操作字符串

  • cstring头文件中提供了许多用于处理C样式字符串的函数,这种处理C样式字符串的方式有什么风险
    答:cstring头文件中用来处理C样式字符串的函数,它们的操作取决于标记字符串末尾的空字符,若空字符被省略或覆盖,就会操作字符串尾部后面的内存,直到遇到空字符串或出现故障,会导致内存被随意覆盖
  • 定义string对象的6种方式
    std::string empty;											//empty为一个不包含字符的字符串
    std::string proverb {"Many a mickle makes a muckle."};		//以字符串字面量定义
    std::string part_literal {"Least said soonest mended.", 5};	//以字符串字面量的前5个字符定义
    std::string sleeping (6, 'z');								//以6个重复字符z来定义
    std::string sentence {proverb};								//以已有的string对象包含的字符串字面量来初始化
    std::string phrase {proverb, 0, 13};						//以string对象从索引0开始的13个字符来初始化
    
  • 加号“+”连接string对象与字符串、string对象与字符的机制
  • 连接 字符串1、字符串2、string对象 的解决办法
  • 连接string对象与数字的方法
  • 访问字符串中字符的方法
  • 获取string对象的子字符串
  • 可用于比较字符串的比较运算符及其比较算法
  • string对象的compare()函数的用法
  • 使用substr()函数进行比较
  • p162

第8章 定义函数

  • main()必须在哪一个名称空间中定义
    答:全局名称空间
  • 为什么要跟踪程序在内存的哪个地方进行了函数调用,这些信息保存在哪里
    答:1)可能同时有若干个函数同时执行;2)保存在栈中,这里常称为调用栈,调用栈中记录了所有函数调用的信息以及传送给每个函数的数据的详细信息
  • 函数由函数头和函数体组成,函数头包括什么,什么是函数签名
    答:函数头包括返回类型、函数名、参数列表;函数名和参数列表的组合称为函数签名
  • 函数体中定义的变量都是局部变量,该规则的例外是什么变量
    答:定义为static的变量
  • 非void返回类型的函数必须返回一个值(存有return语句),该规则的例外是什么函数
    答:main()函数,对于main()函数,执行到右花括号相当于返回0
  • return语句所返回的变量为一个自动变量,那么它的值是如何返回的呢
    答:系统自动复制返回值的一个副本,该副本对调用函数来说是可用的。
  • 返回类型为void时,return语句怎么写
    答:写为return;,或不写return语句。
  • 什么是函数原型、什么是函数声明
    答:函数原型定义了函数的返回类型、函数名及其参数列表(参数列表内可以只写各参数的类型,但最好包含有描述性的参数名),能够充分描述函数,可以让编译器编译对该函数的调用。
           函数原型有时被称为函数声明,函数的定义也是函数声明。
  • 解释按值传送机制
    答:在调用函数时,编译器会创建实参的副本,并将其存储在调用栈的一个临时位置,在执行代码时,代码中对函数参数的所有引用都被映射到实参的这些临时副本上,执行完函数后,就废弃实参的副本。对于有返回值的函数,值返回的机制如前所述。
  • 按值传送机制下,传送指针、数组的方法
    答:按值传送指针:
                      传送的其实是一个地址,在函数的内部对指针解引用后再操作,可完成对指针指向的值的修改,因此按值传送指针有时无需返回值。
                      指针可以为nullptr,所以在使用指针参数前,要先测试它是否为nullptr。
           按值传送数组:
                      数组实际上不能作为一个实参传送,而是使用数组名将数组的地址传送给函数
                      因为数组参数只是数组的地址,所以不能在函数内使用sizeof运算符或std::size()函数来避免指定count参数;
                      编译器不会检查数组实参的实际维度(第一维长度),若参数对其指定,如(double array[10]),在实际传入的数组长度小于10时,程序会读取超过数组边界的值,因此也不能在数组参数中指定维数和索引大小。
    //指针
    double changeIt(double* pointer_to_it);
    double it {5.0};
    double result {changeIt(&it)};
    
    //数组
    double average(double array[], size_t count);		//使用数组表示法
    double average(double* array, size_t count);		//使用指针表示法。编译器认为这两个函数原型完全相同
    double values[] {0.2, 1.2, 2.45, 3.78, 4.1, 5.3};	
    std::cout << "Average = " << average(values, std::size(values) ) << std::endl;
    
  • 按值传送机制下,如何确保函数不会修改数组元素
    答:使用const声明参数
  • 按值传送机制下,如何传送多维数组,参数列表中数组维数如何设置
    答:如前所述,编译器会忽略指定的维度,但第二个维度大小可能得到期望的效果(因为内层维度是外层数组的元素);
           在函数的内部实现中,数组表示法比指针表示法更清楚。数组表示array[i][j],指针表示*(*(array+i)+j)
    double yield(const double values[][4], size_t n);	
    double beans[3][4]{
    	1.0,  2.0,  3.0,  4.0,
    	5.0,  6.0,  7.0,  8.0, 
    	9.0,  10.0, 11.0, 12.0 };
    std::cout << "Yield = " << yield(beans, std::size(beans)) << std::endl;
    
  • 解释按引用传送机制
    答:将参数类型指定为引用,在调用函数时,对应于引用参数的实参不会复制;
             编译器编译引用的方式与编译指针的方式相同;
             在使用时不需要地址和解引用运算符,不需要担心nullptr
  • 函数参数是否为const,对实参的影响
    答:对于函数不会修改的实参,总应该将其声明为const
           如,对于一个函数调用do_it(it);,不看其定义或声明就不知道函数实参是按值传送还是按引用传送,也不知道是否会修改it的值。
  • 什么是输入-输出参数,如何看待输入-输出参数
    答:同时作为输入和输出的参数;最好避免使用这种参数,使每个参数只负责一个用途会使代码更易读
  • 输入、输出参数的建议传送方式
    答:输入参数通常应该是对const的引用,只有比较小的值(主要是基本类型的值)应该按值传送,对于要修改的参数,使用指向非const值的指针;
             只对输出参数使用对非const值的引用,且对于要输出的值,应优先考虑使用返回值。
  • 如何按引用传送数组
    double average10(const double (&array)[10]) {		//按引用传送数组可以指定数组的第一维大小
    		double  sum {};
    		for  (size_t i {}; i < 10; ++i) {
    				sum += array[i];
    		return sum/10;
    }
    double values[] {1.0, 2.0, 3.0};	
    std::cout << "Average = " << average10(values) << std::endl;	//此时编译器会检测传入数组的长度,传入的数组与需要的数组长度不同时会报错
    
  • 参数与实参类型不同时,按引用传送机制是如何进行隐式转换的?哪种隐式转换是合法的(对参数类型的要求)
    答:首先,按值传送的隐式转换并没有特殊之处;
             对于按引用传送(以下面代码为例),
                      对于引用const值的参数:编译器在调用print_it()之前,会在内存的某个位置隐式地创建一个临时的double,以存储转换后的int值,然后把临时内存位置的引用传送给print_it()
                      对于引用非const值的参数:不支持上述形式的隐式转换。(若也进行上述转换,则执行完double_it(i);后,会存在1个值为246.0的临时double变量和值仍为123的int变量i,在继续的操作要将double类型的246.0转换到int赋给变量i,而这是不允许的。)
    void double_it(double& it) { it *= 2; }
    void print_it(const double& it) { std::cout << it << std::endl; }
    int i {123};
    //double_it(i);	/*error, does not compile! */
    print_it(i);
    
  • 按引用传送机制下,字符串字面量实参到string引用的形参是如何进行隐式转换的(对const std::string&类型的形参输入字符串字面量实参)
    答:字符串字面量(类型为const char[]) -> 使用字符串字面量的一个副本实例化一个临时std::string对象。
             可见其中过程存在字符串实参的复制,即使用const std::string&类型传递参数无法完全避免函数不复制输入的字符串参数
  • 如何创建不会复制输入字符串实参的函数
    答:为输入参数使用std::string_view类型(C++17标准库新增的string_view头文件中定义的一个类型)。
             该类型只需指向某个实际的std::string对象、字符串字面量或其它字符数组中存储的任何字符序列即可,任何时候都不能通过它们的公共接口修改它们封装的字符串。因此复制该类型的成本很低,直接使用按值传送std::string_view即可,无需使用const std::string_view&传递参数。
  • 默认实参值
    答:void show_error(string_view message = "Program Error");
             若创建了函数原型和函数定义,则在函数原型中指定默认实参;
             对于引用非const值的参数,若对默认值进行隐式转换需要创建临时对象,则该默认值是非法的;
             多个有默认值的参数必须一直放在参数列表最后,越可能省略的越往后放。
  • main()函数的参数argc、argv
    答:可以将main()函数定义为在运行程序时接收从命令行输入的实参,有参数的main()函数定义如下:
           int main (int argc, char* argv[]) { }
           第一个参数argc是从命令行输入的字符串实参的个数;
           第二个参数argv是一个指针数组,指向命令行实参,包括程序名,数组类型表示的所有命令行实参都接收为C样式的字符串。调用程序时使用的程序名记录在argv[0]中,argv[argc]总是nullptr,即argv中元素个数为argc+1。
  • 从函数中返回指针的注意事项
    答:必须包含nullptr,或者调用函数中仍旧有效的地址,即在指针返回到调用函数时,指针指向的变量仍在其作用域中。
  • 从函数中返回引用的注意事项
    答:不能从函数中返回自动局部变量的引用
  • 返回值与输出参数的使用场合
    答:一般情况首选返回值,对于数组或包含数组的对象使用输出参数
  • 如何使用auto推断一个引用返回类型
    答:使用 auto& 或 const auto&
  • std::optional re
    答:作为返回类型,可用于处理一个可能失败的函数。定义了 operator*() 和 operator->() 运算符,可使用re*rere.value()来取出值;通常联合std::nulloptstd::optional::value_or(default_value)一起使用。
  • 静态变量的使用
    答:静态变量只在第一次调用时,创建和初始化一次,以后就不再执行该语句
  • 内联函数
    答:使用inline进行声明的函数,该关键字在编译时会建议编译器使用内联代码代替函数调用
  • 函数重载
    答:一组函数名相同的函数,以参数列表的区别来区分。且对于默认实参值,必须确保所有函数调用都可以唯一地标识应调用的函数。
    void do_it(std::string number);
    void do_it(std::string& number);							//type与type&,不能区分
    //-----------------------------------------
    long larger(long a, long b);										//对于按值引用,函数是不会改变实参值的,因此编译器会忽略按值引用中基本类型的const声明
    long larger(const long a, const long b);				//因此,对于基本类型,有无const,不能区分
    //-----------------------------------------
    long* larger(long* a,  long* b);	
    long* larger( long* const a,  long* const b);		//同上,指针变量中所存储的地址也不会改变,因此,指针是否声明为const,无法区分
    //-----------------------------------------
    int largest(int* pvalues, size_t count);
    int largest(float* pvalues, size_t count);			//指向不同类型的指针,可区分
    //----------------------------------------
    long* larger( long* a,  long* b);	
    const long* larger( const long* a,  const long* b);	//对指向的值加const,可禁止修改该地址中的值。指向的值是否为const,可区分。
    //----------------------------------------
    long& larger(long& a, long& b);							//引用是对一个确定的变量的别名,不可更改,这一层面上相当于已经是常量,因此不能在&后加const
    long larger(const long& a, const long& b);		//但同上一条一样,T& 和 constT& 是不同的,可区分。	
    
  • 相互递归函数的实现

第9章 函数模板

  • 函数模板
    答:函数模板是函数的参数化定义,从函数模板中生成的函数定义称为模板的一个实例或模板的实例化。函数模板的参数通常是数据类型,作为类型的占位符,但不一定是类型,也可以是其他内容,如维度。
    template <typename T> 			//关键字template将这段代码标识为模板;typename将其后的参数列表(本例只有一个T)中的模板参数标识为类型,
    T larger(T a, T b){					//T为模板参数,作为类型的占位符,可用在具体类型的任何上下文中(函数签名、返回类型和函数体的任何位置)
    	return a>b ? a : b;										
    }
    std::cout<<larger(1.5, 2.5)<<std::endl;
    
  • 创建函数模板的实例
    答:编译器从函数调用的实参中推断用以替代T的类型(模板实参推断),之后编译器会搜索相关类型的现有定义,若没有找到,则创建该实例,每个模板实例只生成一次。
    -如何显式地指定模板实参,以显式地实例化模板
    答: 如下。编译器会使用double类型的函数实例,并将实参20隐式转换为double类型
    larger<double>(20, 19.6);
    
  • 函数模板重载
    答:为特定情况定义重载,可在编译器使用时优先于模板实例,每个重载的函数也都必须有唯一的签名。
  • ①以特例重载】函数模板的特例
    答:对于某个(组)特定参数值,模板的特例定义了不同于标准模板的行为;
           特例的定义必须放在原始模板的声明或定义之后;
           特例的定义以关键字template开头,但要省略参数;
           必须定义特例的实参类型,并放在模板函数名后面的尖括号中。
    template <>
    int* larger<int*> (int* a, int* b) {
    	return *a > *b ? a : b;
    }
    
  • ②以函数重载】此处使用函数定义,而没有用特例定义
    int* larger(int* a, int* b){
    	return *a > *b ? a : b;
    }
    
  • ③以模板重载原有的模板
    template <typename T>
    T larger(const T data[], size_t count){
    	T result {data[0]};
    	for (size_t i {1}; i < count; ++i)
    		if (data[i] > result) result = data[i];
    	return result;
    }
    
  • ④以为指针类型定义的另一个模板来重载原来的模板
    template<typename T>
    T* larger(T* a, T* b) {
    	return *a > *b ? a : b;
    }
    
  • 有多个参数的函数模板
    答:模板具有多个参数时,显式指定模板实参时的顺序要和参数列表中的一致。
    template <typename TReturn, typename TArg1, typename TArg2>
    TReturn larger(TArg1 a, TArg2 b){
    	return a > b ? a : b;
    }
    larger<size_t, double>(1.5, 2);	//将返回类型指定为size_t,第二个类型TArg1指定为double
    
  • 模板的返回类型推断
    答:
    (1) 使用auto关键字(C++14中引入)
    template <typename T1, typename T2>
    auto larger(T1 a, T2 b){
    	return a > b ? a : b;
    }
    
    (2) 使用decltype的拖尾返回类型语法
    template <typename T1, typename T2>					//decltype(expression)可得到expression计算结果的类型,decltype并不会实际计算expression
    //decltype(a > b ? a : b) larger(T1 a, T2 b){		//编译不会通过。因为编译器是从左往右处理模板,因此当decltype处理返回类型时,编译器还不知道a,b的类型
    auto larger(T1 a, T2 b) -> decltype(a > b ? a : b)	//使用拖尾返回类型语法,可将返回类型规范放到参数列表后面,此处的auto用于告知编译器返回类型规范将出现在函数头最后。
    	return a > b ? a : b;
    }
    
    (3) decltype(auto),该语法从C++14引入,用于简略(2)的语法。
    template <typename T1, typename T2>
    decltype(auto) larger(T1 a, T2 b){
    	return a > b ? a : b;
    }
    
    上面,拖尾decltype()和decltype(auto)会推断为引用类型,且会保留const修饰符。而auto问题推断为值类型,即会不可避免地复制值
  • 模板参数的默认值
    答:可以为模板参数列表指定默认值,其位置没有严格要求;当使用一个模板参数作为另一个参数的默认值时,前者在列表中的位置应更加靠前。
    template<typename TReturn=double, typename TArg1, typename TArg2>	//在实参列表一开始指定默认值
    TReturn larger(const TArg1& ,const TArg2&);
    //------------------------------
    template< typename TArg, typename TReturn=TArg>						//使用一个模板参数作为另一个参数的默认值
    TReturn larger(const TArg& ,const TArg&);
    
  • 非类型的模板参数
    答:在定义模板时,非类型的模板参数(和其他的类型参数一起)放在参数列表中,并且,在使用时必须显式指定非类型模板参数。
    template<int lower, int upper, typename T>
    bool is_in_range(const T&value){
    	return (value <= upper) && (value >= lower);
    }
    std::cout << is_in_range<0, 500>(value);		
    
    这种方法只能在编译期间提供上下限,但更好的方法是为上下限使用函数参数
    模板实参推断特别强大,甚至能够从传送给模板的实参的类型推断出非类型的模板实参N
    template <typename T, size_t N>
    T average(const T (&array)[N]){
    	T sum{};
    	for (size_t i {}; i < N; ++i)
    		sum += array[i];
    	return sum / N;
    }
    //---------------------
    double moreDouble[] {1.0, 2.0, 3.0, 4.0};
    std::cout << average(moreDoubles) << std::endl;		//在没有显式指定数组维数时,函数也能工作
    //---------------------
    //double* pointer = moreDoubles;
    //std::cout << average(pointer) << std::endl;		//编译不会通过。编译器无法从指针推断数组大小
    //---------------------
    std::cout << average({1.0, 2.0, 3.0, 4.0};) << std::endl;	//重用了上次的模板实例
    

第10章 程序文件和预处理指令

  • C/C++编译流程
    答:C语言编译过程详解
    ① 预处理(Preprocessing):用于将所有的#include头文件以及宏定义替换成其真正的内容。

    $ g++  -E  test1.cpp  -o  test1.i 
    $ g++  -E  test2.cpp  -o  test2.i 
    

    ② 编译(Compilation):这里的编译不是指程序从源文件到二进制程序的全部过程,而是指将经过预处理之后的程序转换成特定汇编代码(assembly code)的过程。

    $ g++ -S test1.i -o test1.s
    $ g++ -S test2.i -o test2.s
    

    ③ 汇编(Assemble):将上一步的汇编代码转换成机器码(machine code),这一步产生的文件叫做目标文件,是二进制格式。

    $ g++  -c  test1.s  -o  test1.o
    $ g++  -c  test2.s  -o  test2.o
    

    ④ 链接(Linking):链接过程将多个目标文以及所需的库文件(.so等)链接成最终的可执行文件(executable file)。

    $ g++ test1.o  test2.o  -o  test.out
    

    C++17入门经典_第1张图片

  • g++的命令选项
    答: 参考g++入门教程、man g++

    gcc [-c|-S|-E] [-std=standard]							//-E 预处理;-S编译生成汇编代码;-c汇编生成目标文件;-std指定语言版本
    	    [-g] [-pg] [-Olevel]										//-g编译时生成调试信息;
    	    [-Wwarn...] [-pedantic]
    	    [-I incdir...] [-L libdir...]								//-I 设置头文件目录;-L 设置库文件目录
    	    [-Dmacro[=defn]...] [-Umacro]				//
    	    [-foption...] [-mmachine-option...]		//
    	    [-o outfile] [@file] infile...						//-o 指定输出文件名;@file 用于从文件中读取命令行选项;infile...为输入文件列表
    ------------------------------------------------
    #对于`#include "file"`,若使用`-I`指定了头文件目录`incdir`,gcc/g++会先在`incdir`下查找头文件`file`,否则在当前目录下查找,若未找到,再到系统默认的头文件目录下查找。
    #对于`#include `,若使用`-I`指定了头文件目录`incdir`,gcc/g++会先在`incdir`下查找头文件`file`,否则直接到系统默认的头文件目录查找。
    #选项`-include [file]`	相当于代码中的`#include`,用于包含某个代码。
    #举例:   
    $ gcc -std=c++17  -g  -I ./include  -L ./lib  -include /usr/include/pianopan.h   -o  hello1.cpp  hello2.cpp
    
  • 头文件 ( .h )、静态库 ( .lib , .a ) 和共享库 ( .dll , .so )
    答:GCC and Make Compiling, Linking and Building C/C++ Applications
    ①静态库 vs. 动态库
             是预编译目标文件的集合,可以通过链接器链接到您的程序中。示例是系统函数,例如 printf() 和 sqrt()。有两种类型的外部库:静态库和共享库:
             静态库(static library) 在 Unix 中的文件扩展名为".a" (archive file),在 Windows 中的文件扩展名为".lib"(library)。当您的程序链接到静态库时,程序中使用的外部函数的机器代码将复制到可执行文件中。
             共享库(shared library) 在 Unix 中的文件扩展名为".so" (shared objects) ,在 Windows 的文件扩展名为".dll"(dynamic link library) 。当您的程序链接到共享库时,只会在可执行文件中创建一个小表。在可执行文件开始运行之前,操作系统会加载外部函数所需的机器代码——这个过程称为动态链接。动态链接使可执行文件更小并节省磁盘空间,因为一个库的一个副本可以在多个程序之间共享。此外,大多数操作系统允许所有正在运行的程序使用内存中共享库的同一副本,从而节省内存。升级共享库代码无需重新编译程序。
    由于动态链接的优势,默认情况下,GCC 会链接到可用的共享库。

    ②搜索头文件和库(-I, -L and -l)
             编译程序时,编译器需要头文件来编译源代码;链接器需要库来解析来自其他目标文件或库的外部引用。
             对于源代码中使用的每个头文件(通过 #include 指令),编译器会在所谓的包含路径中搜索这些头文件。包含路径是通过 -Idir 选项(或环境变量 CPATH)指定的。由于头文件名是已知的(例如,iostream.h、stdio.h),编译器只需要目录
             链接器在库路径中搜索将程序链接到可执行文件时所需的库。库路径通过 -Ldir 选项(大写"L"后跟目录路径)(或环境变量 LIBRARY_PATH)指定。此外,您还必须指定库名称。在 Unix 中,库 libxxx.a 是通过 -lxxx 选项指定的)小写字母"l",没有前缀"lib"和".a"扩展名)。在 Windows 中,提供全名,例如 -lxxx.lib。链接器需要知道目录和库名称。因此,需要指定两个选项。

  • 转换单元
    答:每个源文件及其所包含的头文件内容称为一个转换单元,即对应一个源文件和若干个头文件;
             编译器独立处理程序中的每个转换单元来生成对象文件,对象文件包含机器码和实体引用的信息;
             链接程序在对象文件之间建立必要的连接, 以生成可执行程序模块;
             编译和链接转换单元合称为“转换”。

  • 单一定义(ODR)规则
    答:任何变量、函数、类类型、枚举类型、概念 (C++20 起)或模板,在每个转换单元中都只允许有一个定义(其中部分可以有多个声明,但只允许有一个定义)。
             在整个程序(包括所有的标准或用户定义的程序库)中,被 ODR 式使用(odr-used意味着在其定义必须存在的环境中使用某些东西(variables或函数))的非 inline 函数或变量只允许有且仅有一个定义。
              inline 函数或 inline 变量 (C++17 起)的定义必须在调用它们的每个转换单元中出现一次,但在所有的转换单元中,给定内联函数和变量的所有定义必须相同。因此应在头文件中定义内联函数和变量,并在需要内联函数或变量的源文件中包含这个头文件
              通常要在多个转换单元中使用类或枚举类型,故允许程序中的几个转换单元分别包含给定类型的定义,但这些定义必须相同。因此,可把类类型的定义放在头文件中,再使用#include指令把头文件添加到需要类型定义的源文件中,但,在同一个转换单元中给定类型的重复定义是非法的。

  • 链接属性
    答:转换单元中的名称在编译链接过程中处理的方式由链接属性确定;
             链接属性指定了由一个名称表示的实体可以在程序代码的什么地方使用;
             当某个名称被用于在声明它的作用域外部访问其程序内容时,就有链接属性;
             如果某个名称有链接属性,就同时有内部链接和外部链接属性;
             内部链接属性表示其实体可以在同一转换单元的任何地方访问;
             外部链接属性表示其实体可以在整个程序中共享和访问;
             没有链接属性表示其实体只能在该名称中作用域中访问。

  • 外部函数
    答:函数名默认具有外部链接属性
             若函数没有在调用它的转换单元中定义,编译器就会为将这个调用标记为外部链接属性,让链接程序处理。
             因此,对包含A函数定义的A.cpp、包含A函数声明(原型)的B.cpp,通过链接即可生成正确的可执行文件。
             但通过把函数原型放到头文件中,然后使用#include指令把头文件包含到转换单元中。

  • 外部变量
    答:(1)非const变量默认具有外部链接属性。若要访问在当前转换单元外部定义的变量,必须使用extern关键字声明变量名称,避免违反ODR规则,也可以在上一条的外部函数声明前使用extern修饰符,以明确指出函数定义在另一个转换单元中。

    //cpp1
    int power_range{3};										//定义一个全局变量
    double power(doublex, int n){...}				//定义一个函数
    //cpp2
    extern int power_range;								//extern必需,全局变量在没有初始化列表时自动初始化为0,加extern避免违反ODR规则
    extern double power(double x, int n);	//extern可选
    

             (2)const变量在默认情况下有内部链接属性,这使它不能在其他转换单元中使用,使用extern关键字可以重写这个属性,

    //cpp1
    extern const int power_range{3};		//定义一个全局常量,注意对const变量添加外部链接属性要在定义时也加入extern
    //cpp2
    extern const int power_range;			//声明
    
  • 内部名称
    答:上面是需要声明外部链接的场合,有时还会有一些只需要在当前转换单元使用的局部辅助函数,但“函数名默认具有外部链接属性”的特性使得它们总会有外部链接属性,也就不能在其他转换单元中定义有相同签名的函数。过去的解决方案是使用static关键字声明函数。在现代C++中:
    任何时候都不要再使用static关键字来标记应该具有内部链接属性的名称;相反,应该总是使用未命名的名称空间。

  • 预处理指令
    答:#字符串化运算符;##连接运算符
             #define IDENTIFIER sequence of characters
             该指令将宏标识符IDENTIFIER替换为一个字符串,以前常用于定义符号常量、创建类似于函数的宏。但在现代C++中:
             总不要使用预处理宏来定义符号常量和简单函数,而总应该使用const常量和普通的C++函数或函数模板

  • 头文件
    答:在源文件中包含自己的头文件时,常用双引号:#include "myheader.h";
             多个或多级头文件包含时,有些头文件可能会被多次包含到一个源文件中;#include指令的无限递归会导致编译失败。可使用下面的 #include保护符

    //对于头文件myheader.h
    #ifndef MYHEADER_H
    #define MYHEADER_h
    //myheader.h中的所有代码放在这里
    #endif
    
  • 名称空间
    答:如果没有定义名称空间,就默认使用全局名称空间;
             当存在同名的局部声明覆盖了全局名称时,就需要使用作用域解析运算符显式地访问全局名称:::power(2.0, 3)
             在名称空间中不能包含main()函数;
             如果要把函数放在名称空间中,只需要把函数的原型放在名称空间中即可,函数可以在其他地方定义,但要使用限定过的名称
             当名称空间较长时,可以为其定义一个别名:

    namespace alias_name = original_namespace_name;
    
    namespace calc{
    	...
    }
    namespace sort{
    	...
    }
    namespace calc{		//扩展名称空间定义
    	...
    }
    
  • 未命名的名称空间
    答:在不给名称空间指定名称时,会由编译器生成一个内部名称;
             在一个转换单元中只能有一个未命名的名称空间,其余没有命名的名称空间都是其扩展;
             未命名的名称空间中声明的所有名称都具有内部链接属性(即使用extern修饰符定义了名称),它们的实体都是定义它们的转换单元中的局部成员。

  • 逻辑预处理指令
    答:逻辑#if指令:可以定义预处理标识符,为要编译的代码指定环境,并据此选择代码或#include指令。在不同的硬件或操作系统环境中运行或维护应用程序时非常有用。

    #if constant_expression
    		//
    #endif
    # if LANGUAGE==EN
    		//
    #elif LANGUAGE==CN
    		//
    #else
    		//
    #endif
    
  • 调试方法
    答:(1)在函数中使用预处理指令#if #endif,以隔离调试所需的代码;
             (2)使用assert()宏。assert(expression)会在expressionfalse时,终止程序,并输出诊断信息。其中的expression可以是任意逻辑表达式。assert()的头文件是,通过在cassert的#include语句前添加#define NDEBUG可关闭预处理断言机制,忽略转换单元中所有断言语句。
             (3)静态断言:
                      静态断言是语言内置的部分,用于在编译时静态检查条件;
                      当constant_expression是false时,程序会编译失败,若提供了error_message,就会输出一条包含它的诊断消息,否则编译器将基于constant_expression生成一个;当constant_expression为true时,静态断言什么也不会做;
                      静态断言的一个常见用途是在模板定义中验证模板参数的特征。

    static_assert(constant_expression, error_message);
    static_assert(constant_expression);		//C++17新加,省略error_message字符串字面量
    //--------------------------
    static_assert(std::is_arithmetic_v(T), "Type parameter for average() must be arithmetic");
    

第11章 定义自己的数据类型

  • 面向对对象编程(OOP)
    答:根据要解决的问题范围内所涉及的对象来编写程序,因此程序开发过程的一部分是设计一组类型来满足这个要求

  • 封装
    答:给定类的每个对象都组合了下述内容:
                      一组数据值,作为类的成员变量,指定对象的属性;
                      一组操作,作为类的成员函数。
                      把这些数据值和函数打包到一个对象中,就称为封装
             在一般情况下,不允许访问对象的数据值,这就是数据隐藏,数据成员一般不能从外部访问;
             隐藏对象中的数据,可以禁止直接访问该数据,但可以通过对象的成员函数来访问;
             成员变量表示对象的状态,操纵它们的成员函数则表示对象与外界的接口;
             在设计阶段,正确设计类的接口非常重要,以后可以修改其实现方式,而不需要对使用类的程序进行任何修改;
             建议:以一致的方式隐藏对象的数据,并只通过其接口中的函数来访问和操作对象的数据。

  • 继承
    答:根据类BankAccount定义一个新的LoanAccount叫做继承BankAccount被称为基类LoanAccount派生于BankAccount

  • 多态性
    答:多态性表示在不同的时刻有不同的形态。它意味着属于一组继承性相关的类的对象可以通过基类指针和引用来传送和操作。指向基类的指针变量可以存储该基类及其派生类对象的地址,这组类中会有相同的基类中的成员函数,该指针对其中某个成员函数名的调用会根据所指向对象的不同而调用不同的函数实体,即同一个函数调用会根据指针指向的对象完成不同的操作。

  • 类成员访问修饰符
    答:private: 只能由该类中的函数、其友元函数访问,不能被任何其他访问,该类的对象也不能访问;
             protected: 可以被该类中的函数、子类的函数、以及其友元函数访问,但不能被该类的对象访问;
             public: 可以被该类中的函数、子类的函数、其友元函数访问,也可以由该类的对象访问。
             注:友元函数包括两种:设为友元的全局函数,设为友元类中的成员函数

  • 定义类

    //Box.h
    #ifndef BOX_H
    #define BOX_H
    
    class Box{
    private:
    	double length {1.0};
    	double width {1.0};
    	double height {1.0};
    public:		
    	//Box (double lengthValue = 1.0, double widthValue = 1.0, double heightValue = 1.0);//有初始值的默认构造函数
    	Box(double length, double width, double height);
    	//explicit Box(double size);
    	Box(const Box& box);
    	Box() = default;		//显式生成默认构造函数(在需要无参数的默认构造函数时,推荐)
    	//Box() {}						//或自己定义默认构造函数(不推荐)
    	double volume();
    };			//类定义的右花括号后面必须有分号
    #endif
    
    //------------------------------------------	
    //Box.cpp
    #include "Box.h"
    #include
    //Constructor define
    //成员初始化列表对成员变量赋值
    Box::Box(double lv, double wv, double hv) : length{lv}, width{wv}, height{hv} { }	//最为推荐,注意初始化列表放在定义时
    //Box::Box(double side) : Box{side, side, side} { }			//委托构造函数
    Box::Box(const Box& box) : Box(box.length, box.width, box.height} {}		//副本构造函数、委托构造函数
    double Box::volume(){
    	return length*width*height;
    }
    	
    //------------------------------------------	
    //main.cpp
    #include
    #include"Box.h"
    int main(){
    	Box firstBox {80.0, 50.0, 40.0};	//使用自定义构造函数创建对象
    	double firstBoxVolume {firstBox.volume()};
    	Box secondBox;								//使用默认构造函数创建对象
    	double secondBoxVolume {secondBox.volume()};
    }
    	```
    
  • 构造函数
    答:构造函数用于在创建对象时设置成员变量的值,确保成员变量包含有效的值;
             构造函数常常与包含它的类同名;
             构造函数没有返回值(所以也没有返回类型);
             如果不为类定义任何构造函数,编译器将生成默认构造函数
             默认构造函数没有参数,其唯一作用就是创建对象;
             若使用默认构造函数创建对象,成员变量就会使用默认值,如果没有为指针类型或基本类型的成员变量指定初始值,它们就会包含垃圾值;
             只要用户提供了(任何)构造函数,编译器就不会生成默认构造函数了,若此时仍然想让对象被默认构造,可使用default关键字;
             在定义类时的常见做法是将类放到一个头文件中,将成员函数和构造函数放到对应的源文件中
             可以为类的成员函数指定参数的默认值,构造函数和成员函数的默认实参值总是放在类中,不放在外部构造函数和成员函数中;
             所有参数都有默认值的构造函数算作默认构造函数,即这样的构造函数会与使用default构造的默认构造函数冲突;
             可以在构造函数头后添加成员初始化列表对成员变量赋值,初始化顺序由类定义中声明成员变量的顺序决定。

  • 使用explicit关键字
    答:类的构造函数只有一个参数是有问题的,如

    //对Cube类,其构造函数为:
    Cube(double aside);
    //它的一个成员函数为:
    bool hasLargerVolumeThan(Cube aCube);	//该函数用于比较当前对象与作为参数的aCube对象的体积大小
    //可见上面这个函数的参数应该是一个Cube类型的对象,如下面这样使用:
    Cube box1 {7.0};
    Cube box2 {3.0};
    if (box1.hasLargerVolumeThan(box2))
    		std::cout<< "box1 is larger than box2" <<std:endl;
    //↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
    //但由于构造函数只有一个参数,当成员函数hasLargerVolumeThan的参数与构造函数的参数类型相同或缩窄转换后相同时,对于如下语句:
    //↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑
    if (box1.hasLargerVolumeThan(5.0))
    		std::cout<< "box1 is larger than 5.0" <<std:endl;
    //↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
    //编译器会将实参5.0转换为一个Cube对象:
    //↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑
    if (box1.hasLargerVolumeThan(Cube {5.0}))
    		std::cout<< "box1 is larger than 5.0" <<std:endl;
    //这相当于把box1与一个边长为5.0的box进行了比较,与期望的用法不同
    //把构造函数声明为explicit可避免这种情况:
    expilicit Cube(double aside);
    

    explicit声明用在(头文件)函数定义内的原型中;
    编译器不会把声明为explicit的构造函数用于隐式类型转换,它只能在程序代码中显式创建对象。默认情况下,应该将所有包含一个实参的构造函数(包括有多个参数,但除了第一个参数外其他参数都有默认值的构造函数)声明为explicit

  • 委托构造函数
    答:在初始化列表中调用同一个类中的另一个构造函数完成实例化

  • 副本构造函数
    答:通过复制已有的对象来创建对象;
             若没有定义副本构造函数,编译器会提供一个默认的副本构造函数;
             由于复制指针时,不会复制指针指向的内容,当成员变量中存在指针时,通过副本构造函数创建的对象就会与原对象链接起来,导致两个对象都指向相同内容的成员
             在自定义副本构造函数时,考虑以下问题:
                      使用副本构造函数创建新实例时,若副本构造函数是按值传递,就会创建输入对象的副本,这就又会调用副本构造函数,导致副本构造函数的无限递归调用。因此:
                      副本构造函数的实参必须是const引用(const是由于要保证不改变原对象)。

  • 不用参数创建对象的方法(互相冲突):
    答:default构造函数、
             在构造函数中为所有参数添加默认值、
             为成员变量添加默认值

  • 为什么成员变量要用private声明
    答:暴露出来的接口应该是稳定的,而类中的变量随着需求的变动有可能会增删改,private成员变量可使类的调用更加鲁棒;
             对于非常稳定的成员变量,可以将它们声明为public,可方便其使用。

  • 访问私有类成员
    答:使用访问器函数(getter)来提取成员变量的值;
            使用更改器成员函数(setter)来修改成员变量;

    double getLength() {return length;}
    void setLength(double lv} {if (lv>0) length = lv;}
    
  • this指针
    答:在执行任何成员函数时,该成员函数都会自动包含一个隐藏的指针,称为this指针,该指针包含调用该成员函数的对象的地址;
            一般情况下不必显式使用this指针。

  • 从函数中返回this指针的用法
    答:把成员函数的返回类型指定为类类型的指针或引用,就可以从函数中返回this指针或其解引用。

    Box* Box::setLength(double lv){
    	if (lv>0) length = lv;
    	return this;
    }
    Box* Box::setWidth(double wv){
    	if (wv>0) width = wv;
    	return this;
    }
    Box* Box::setHeight(double hv){
    	if (hv>0) height = hv;
    	return this;
    }
    Box mybox{3.0, 4.0, 5.0};
    mybox.setLength(-20.0)->setWidth(40.0)->setHeight(10.0);		//使用指针的方法链
    //----------------------------------------------
    //----------------------------------------------
    Box& Box::setLength(double lv){
    	if (lv>0) length = lv;
    	return *this;
    }
    Box& Box::setWidth(double wv){
    	if (wv>0) width = wv;
    	return *this;
    }
    Box& Box::setHeight(double hv){
    	if (hv>0) height = hv;
    	return *this;
    }
    Box mybox{3.0, 4.0, 5.0};
    mybox.setLength(-20.0).setWidth(40.0).setHeight(10.0);		//使用引用的方法链
    
  • const对象和const成员函数
    答:类类型的const变量称为const对象,构成const对象状态的任何成员变量都不能被修改;
         这一原则也适用于指向const变量的指针和对const变量的引用;
         对于const对象,只能调用const成员函数,即要把所有不修改对象的函数指定为const;
         在const成员函数内不能调用任何非const成员函数;
         有无const是函数签名的一部分,因此可以用const版本来重载一个非const版本的成员函数;
         添加public成员函数来返回对private成员变量的引用,越过了成员变量的private声明,可以在类外读写private成员变量,但这与简单地将这些变量声明为public一样不好;
         const成员函数隐含的this指针类型是其对象的const指针,即不能修改对象;
         可以使用mutable关键字声明成员变量,以在const对象中修改它;

    cnost Box mybox {3.0, 4.0, 5.0};		//const对象
    Box mybox {3.0, 4.0, 5.0};
    const Box* boxpointer = &mybox;			//指向const变量的指针
    const Box& boxrefer = mybox;			//对const变量的引用
    //----------------------------
    double volume() const;						//把不修改对象的函数指定为const
    double Box:volume() const {return length*width*height;}
    //---------------------------
    double& length() { return _length; }				//public成员函数,返回private成员变量的引用(不推荐)
    double length() const { return _length; }		// const重载
    
  • 友元
    答:友元可以访问类对象的任意成员,无论这些成员的访问修饰符是什么(只有在绝对有必要时才应使用友元);
         类的友元函数要在类定义中用关键字friend来编写函数原型,友元函数可以是一个全局函数或另一个类的成员。
         友元函数不是类成员,所以成员变量必须用对象名来限定;
         友元类的所有成员函数都可以不受限制地访问原有类的成员;

    friend double surfaceArea(const Box& abox);
    friend class Carton;
    
  • 类的对象数组
    答:类对象的数组的每个元素都根据初始化参数由构造函数创建。

  • 类对象的大小
    答:类对象的大小一般是类中成员变量大小的总和,或稍大(这是由于边界对齐导致的)

  • 类的静态成员
    答:静态成员是指声明为static的成员;
         静态成员独立于类类型的所有对象,但可由它们访问;
         静态成员变量只定义一次,无论定义多少个类对象,每个静态成员变量的实例只有一个;
         即使没有创建对象,静态成员变量也存在,可以使用类名限定变量名来使用静态变量;
         同时将静态成员变量声明为inline变量,可在头文件中初始化它们,而不必在源文件中单独进行定义,避免违反ODR;
         静态成员变量常常用于定义常量,可避免每个对象都创建一个该成员变量的副本;
         通常将定义为static和const的所有成员变量也定义为inline,以在类定义中直接初始化;
         对关键字static、inline、const的顺序没有要求;
         可以定义类类型的静态成员变量,但需要在类的外部进行定义和初始化;
         静态成员函数也独立于类的对象,可使用类名调用静态函数;
         由于静态成员函数是所有对象公用的,因此可以通过对象来调用静态成员函数(不推荐),但静态成员函数不能访问调用它的对象;
         若想让静态成员函数访问类的对象,需要把该对象作为参数传递给静态函数;
         由于静态成员函数与对象无关,所以它没有this指针,不能使用const声明。

  • 析构函数
    答:对类对象应用delete运算符或处在创建类对象的块的末尾,就会释放类对象;
         释放类对象会执行析构函数,它与类同名,但名称前有一个~
         如果没有定义析构函数,编译器会提供一个默认的;

    ~Box() {}				//声明
    Box::~Box() = default;		//使用default定义一个默认析构函数
    
  • 使用指针作为类成员
    答:现实中的程序通常由大量彼此协作的对象构成,这些对象通过指针、智能指针和引用链接在一起;
         需要创建这些对象的网络,将它们链接在一起,最后再释放它们(通常使用智能指针)。

  • 嵌套类
    答:在嵌套类内可以访问外层类的私有成员。

第12章 运算符重载

第13章 继承

第14章 多态性

第15章 运行时错误和异常

第16章 类模板

第17章 移动语义

第18章 头等函数

第19章 容器与算法

第20章 STL

20.1 容器(Containers)

type 功能 描述
std::vector<>
封装动态大小数组的序列容器。
.reserve(size_type n) 为向量申请内存空间
.at(size_type n) 返回对位置元素的引用
.erase(iterator) //删除一个元素
vector::erase(iterator position);
//删除一个范围内的元素
vector::erase(iterator start_position, iterator end_position);
它返回一个迭代器,指向由 vector::erase() 函数擦除的最后一个元素后跟的元素。
std::unordered_map .end() 返回一个迭代器,该迭代器指向unordered_map容器中容器中最后一个元素之后的位置
.find(key) 如果给定的键存在于unordered_map中,则它向该元素返回一个迭代器,否则返回映射迭代器的末尾,因此可配合 end()判断某键值对是否在map中
.at(key) 返回对应元素value的引用
.count(key) 检查unordered_map中是否存在具有给定键的元素,如果Map中存在具有给定键的值,则此函数返回1,否则返回0。
.emplace() 向容器中添加新键值对,效率比 insert() 方法高。
.empty() 若容器为空,则返回 true;否则 false。
.size() 返回当前容器中存有键值对的个数。
std::set
按照特定顺序存储;
关联容器,key就是value,key唯一;
.insert() 在集合容器中插入元素

答:

算法(Algorithms)

迭代器(iterators)

Eigen

  • Eigen内存分配器Eigen::aligned_allocator
    在使用Eigen的时候,如果STL容器中的元素是Eigen数据库结构,比如下面用vector容器存储Eigen::Matrix4f类型或用map存储Eigen::Vector4f数据类型时:

    std::vector<Eigen::Matrix4d>;
    std::map<int, Eigen::Vector4f>;
    

    这么使用编译能通过,但运行时会报段错误。

    对eigen中的固定大小的类使用STL容器的时候,如果直接使用会出错,所谓固定大小(fixed-size)的类是指在编译过程中就已经分配好内存空间的类,为了提高运算速度,对于SSE或者AltiVec指令集,向量化必须要求向量是以16字节即128bit对齐的方式分配内存空间,所以针对这个问题,容器需要使用eigen自己定义的内存分配器,即aligned_allocator。

    这个分配器所在头文件为:

    #include 
    

    根据STL容器的模板类,比如vector的声明:

    template<typename _Tp, typename _Alloc = allocator<_Tp> >
    class vector : protected _Vector_base<_Tp, _Alloc> {
        .....
    }
    

    使用aligned_alloctor分配器,上面的例子正确写法为:

    //std::vector;
    //std::map;
    std::vector<Eigen::Matrix4d,Eigen::aligned_allocator<Eigen::Matrix4d>>;
    std::map<int, Eigen::Vector4f, Eigen::aligned_allocator<std::pair<const int, Eigen::Vector4f>>;
    

    上述的这段代码才是标准的定义容器方法,只是我们一般情况下定义容器的元素都是C++中的类型,所以可以省略,这是因为在C++11标准中,aligned_allocator管理C++中的各种数据类型的内存方法是一样的,可以不需要着重写出来。但是在Eigen管理内存和C++11中的方法是不一样的,所以需要单独强调元素的内存分配和管理。

  • std::allocate_shared
    用法:std::shared_ptr<类型A> 指针变量B = std::allocate_shared<类型A> (类型A的allocator, 指针变量B所指向的类型A的参数列表);

    std::shared_ptr<std::pair<int,int>> baz = std::allocate_shared<std::pair<int,int>> (std::allocator<int>,30,40);
    //类型Frame中含有Eigen类型的变量
    std::shared_ptr<Frame> pkf = std::allocate_shared<Frame>(Eigen::aligned_allocator<Frame>(), *pcurframe_);
    

你可能感兴趣的:(其他)