C++prime读书笔记(一)C++基础


layout: post
title: C++prime读书笔记(一)C++基础
description: C++prime读书笔记(一)C++基础
tag: 读书笔记


第1章:开始

1.2 输入输出

iostream库包含两个基础类型istream和ostream,输入流和输出流,一个流就是一个字符序列。

输入cin 输出cout 错误cerr(输出警告和错误信息,例如在内存溢出时,借内存写错误信息) 日志clog
除了cin,剩余三个用法是一致的。

	std::cout << "Enter two numbers:" << std::endl;
	int v1, v2;
	std::cin >> v1 >> v2;
	if (v1 == 0) 
	{
		std::cerr << "input can not be zero" << std::endl;
	}
	std::clog << "compute result" << std::endl;
	std::cout << v1 + v2 << std::endl;
	
	return 0;

第2章:变量和基本类型

2.1 基本内置类型

  1. 主要需注意的是无符号类型(unsigned),即不包含符号位,所有位均用于编码数字,故可编码范围有所不同。
    当给无符号类型赋值一个超出它表示范围的值时,结果是初始值对它表示范围总数取模后的余数。
    例如 unsigned char c = -1 // 假设char占8比特,c的值为255
    8位表示的范围总数为256,-1取模余数为255
  2. 带符号类型超出范围结果是未定义!int long short都是

2.2 变量

  1. 变量的列表初始化:采用花括号给变量初始化赋值
  2. 变量的初始化:变量的初始化不是赋值,初始化是创建变量时机给予一个初始值,赋值的含义则是把对象当前值擦除,以一个新值来代替。如果定义变量时没有指明初值,变量则会被默认初始化,例如不指定初值的string,默认为空字符串。
  3. 变量声明与定义:声明使得名字为程序所知,使得代码在别处可以识别和使用。定义则负责创建与声明的名字关联的实体。变量能且只能被定义一次,但是可以多次声明。
    如果想声明一个变量而非定义它,需要用到关键字extern表示该变量或者函数在别的文件中已经定义,提示编译器在编译时要从别的文件中寻找。使用extern 时不要初始化变量,否则extern则会失去作用。
extern int i ; // 声明i
int i; //声明并定义 i

C++支持分离式的编译,如果在多个文件中使用同一个变量,必须将声明和定义分离,定义必须出现且只能出现在一个文件中,而其他用到该变量的文件必须对其声明,但不可以重复定义。

2.3 复合类型

2.3.1 引用

  1. 引用是给已有对象起别名,引用并非对象,可以理解为一条指向已有对象的线。
  2. 引用必须被初始化。后边学了const后会更加理解这一点,const修饰的变量一旦定义不可更改,故必须在定义时机便初始化值,而引用的本质可以理解为一个指针常量(指针中的常量,指向不可更改),即是常量则必须在引用定义时指明要引用的对象,且后边不可以再更改。
  3. 因为引用并非对象,故不可定义引用的引用,而指针则可以。
  4. 允许在一条语句中定义多个引用,其中每个引用标识符都必须以符号&开头
  5. 引用类型的初始值必须为一个对象,不可以是常量。(常量引用是例外)(指针则可以赋予常量初始值,比如:0)
  6. 一般情况下引用的类型要与初始值类型保持一致。(有两种例外情况:a、初始化常量引用时,允许用任意表达式作为初始值,只要表达式的结果能够转成引用的类型即可。b、存在继承关系的类是另一个重要的例外,我们可以将基类的指针或引用绑定到派生类对象上。)
  7. 一般情况下(特例也是常量引用),引用只能绑定在对象上,而不能与字面值或某个表达式的计算结果绑定在一起。
int ival = 1024;
int &refVal = ival; // refval是ival的别名
int &refVal; // 报错:引用必须初始化。
int i1 = 1024, i2 = 2048;
int &r1 = i1, &r2 = i2;
int i3 = 56, &r3 = i3; //也可以在同一行定义并引用

int &r4 = 10; // 报错:引用初始值必须是对象
double i = 5.0;
int &r5 = i; // 报错:引用类型与初始值i的类型不一致

int  i5 = 45;
const int &r5 = i5; //允许将const int& 绑定到普通的int对象上
const int &r6 = 42; // 常量引用初始化可以是常量, 5中提到的特例
const int &r7 = 2*r5;
int &r8 = r5*2; // 普通类型的int&不可绑定到常量引用。


13.6.1 右值引用

C++11,引入了右值引用,即必须绑定到右值的引用,通过&& 而非&获得右值引用。右值引用的重要性质是——它只能绑定到一个将要销毁的对象上,由此实现将一个右值引用的资源“移动”到另一个对象中。
左值引用我们一般不能将其绑定到要求转换的表达式、字面常量或是返回右值的表达式。而右值引用有着完全相反的绑定特性:我们可以将一个右值引用绑定到这类表达式上,但不能将一个右值引用直接绑定到一个左值上。

int i = 42;
int &r = i;
int &&rr = i; // 报错:不能将一个右值引用绑定到一个左值上。
int &r2 = i * 42; // 报错:i*42是一个右值表达式
const int &r3 = i * 42; // 正确,const引用是可以绑定到右值表达式的
int &&rr2 = i * 42; // 正确:将rr2绑定到乘法计算结果上

右值引用只能绑定临时对象

  • 所引用的对象将要被销毁
  • 该对象没有其他用户

这两个特性意味着:使用右值引用的代码可以自由接管所引用对象的资源。

2.3.2 指针

  1. 指针存放某个对象的地址,指针本身也是一个对象,指针本身有自己的地址,故可以定义指针的指针。
  2. 一般情况下,指针类型要与它指向的对象严格匹配(特例:a、const类型指针可以指向非const类型对象。b、基类指针可以指向派生类的对象)这两个特例实际上与引用中类型匹配是一样的
  3. 空指针生成方式:
    • 用nullptr初始化,它是一种特殊类型的字面值,可以转换为任意其他类型的指针类型。
    • 用字面值0赋值或者用NULL赋值,NULL实质上就是0。它是一个预处理变量。
  4. void* 指针
    可用于存放任意类型对象的地址,但因为不知道对象的类型,不可访问和操作所指对象。可以用作函数的输入输出,与其他指针比较以及赋值给另一个void *指针

2.3.3 理解复合类型的声明

  1. 变量的定义包含一个基本数据类型和一组声明符,在同一条定义的语句中,虽然基本数据类型只有一个,但是声明符的形式(*, &)却可以不相同。
  2. 类型修饰符是声明符的一部分,只对单个变量修饰。例如下边的第二条语句:int* p1, p2;
    这种写法误导性很强,这里实际的结果是p1是指向int的指针,p2则是int类别的变量而已,*只对p1有作用。
    最好的做法是固定一种写法,例如prime中固定将类型修饰符*,&与变量名连在一起
int i = 1024, *p = &i, &r = i;
int* p1, p2; // p1是指向int的指针,p2则是int类别的变量而已
int *p1, p2; // 与上边写法等价

2.4 const限定符

  1. const对象一旦创建后,其值就不能再改变,故const对象必须初始化,初始值可以是任意复杂的表达式。
  2. const对象可以用来初始化非const对象,实际上初始化只是拷贝对象的值,故不必在意对象的const属性。
  3. 默认状态下,const对象仅在文件内有效,如果变量需要在多个文件内有效,为避免重复定义,可以在声明和定义时的前边添加extern关键字。
2.4.1 const引用
  1. const引用,常常简称为常量引用,常量指针常量,指向与所指的值都不可修改。
  2. 非常量引用不可指向常量对象,但反之,常量对象可以引用非常量对象.
  3. 可以用字面值或者表达式初始化常量引用。
  4. 可以用其他数据类型的结构初始化常量引用,程序会先自动执行类型转换。例如下边最后一条语句,程序会自动执行const int temp = dval;再将temp赋值给引用const int &r6 = temp;
  5. 常量引用仅对引用可以参与的操作进行限定,引用对象本身仍然可以通过其他途径修改,这一点与常量的指针是一致的。(所谓指向常量的引用或指针,不过是指针或引用 “自以为是”的觉得自己指向了常量,所以自觉不去修改它,实际上,对象可不可以修改,取决于对象本身是否为常量,如果不是,对象仍然可以通过其他途径修改。)
int i = 42;
const int &r1 = i; // 常量引用可以绑定普通int对象
const int &r2 = 42;// 可以用字面值或者表达式初始化常量引用。
const int &r3 = r1 * 2;// 可以用字面值或者表达式初始化常量引用。
int &r4 = r1 * 2; // 报错:非常量引用则不可用字面值或者表达式初始化常量引用。
int &r5 = ci // 报错:非常量引用不可以引用常量对象
double dval = 3.14;
const int &r6 = dval; //  可以用其他数据类型的结构初始化常量引用,程序会先自动执行类型转换。 
const double pi = 3.14;
const double *cptr = &pi; // pi本身是常量,不可以修改pi
double dval = 3.14;
cptr = &dval; // dval 本身并没有被定义为常量,而仍然可以用来初始化cptr,不可以通过cptr修改dval的值,但可以通过其他方式修改
2.4.2 const指针

指针是一个对象,const声明的指针必须初始化,一旦初始化后,则它的值(存放在指针中的地址)就不能再改变了(指向不可变)。将*放在const前,代表指针常量,即指针中的常量,指向不可更改。
const指针所指对象能否修改,不取决于指针是否为常量指针,而取决于所指对象本身是否为常量。

int errNumb = 0;
int *const curErr = &errNumb; // curErr 将一直指向errNumb
const double pi = 3.14;
const double *const pip = &pi; // pip是一个指向常量double对象的指针常量
2.4.3 顶层const

指针本身是一个对象,又可以指向另一个对象,指针本身是否为常量和所指对象是否为常量是两个独立的问题,用顶层const表示指针本身是个常量,而用底层const表示指针所指对象为一个常量。顶层const表示指针存放的地址不可更改,即,指向不可更改,而底层const表示指针指向的对象不可更改。
顶层const: *const,指针常量,指针中的常量,指向不可修改。
底层const:const (int)✳, 常量指针,常量的指针,所指对象视为常量,不可改变所指对象。

int i =0;
int *const p1 = &i; // 顶层const,不可改变p1
const int ci = 42;
const int *p2 = &ci; // 允许改变p2,不可改变ci,这是底层const
const int *const p3 = p2; // 靠右的const是顶层const,靠左的const是底层const
const int &r = ci; // 用于声明引用的const都是底层const

当执行对象的拷贝操作时,顶层const和底层const区别明显。
其中顶层const不受影响,底层const的限制不可忽视。
一般来讲,非常量可以转为常量,而常量不可转为非常量。

i = ci;  // 正确
p2 = p3; // p2和p3所指对象类型相同,p3顶层const的部分不受影响。
int *p = p3; // 错误,p3包含底层const的含义,即所指对象不可修改,而p没有,如果这样定义,可能出现通过p修改常量的错误操作
p2 = p3; // 正确,p2和p3都是底层const
p2 = &i; // 正确,int * 可以转成 const int *
int &r = ci; // 错误,普通的int不可以绑定到int常量上,如果这样做,可能出现通过r修改常量ci的错误操作
const int &r2 = i; // 正确,const int & 可以绑定到一个普通的int上。

总结:
事实上底层const出现限制的原因主要在于由于底层const所指对象为常量,故绑定引用或者给指针赋值时,可能出现通过该引用或指针去修改常量的后果,出现这种后果的拷贝操作都是错误的

2.4.4 constexpr和常量表达式

常量表达式:值不会改变且在编译过程中即可算出结果的表达式,字面值是最基本、最简单的常量表达式。
constexpr :
C++11允许将变量声明为constexpr类型以便于由编译器来验证变量值是否是常量表达式。在新标准下可以定义一种特殊的constexpr函数去初始化一个constexpr变量。
注意:如果在consexpr声明中定义了一个指针,限定符constexpr仅对指针有效,与指针所指对象无关。

2.5处理类型

  1. 使用关键字typedef给变量类型起别名:typedef double wages;// wages是double的同义词
    typedef wages base, *p; // base 是double的同义词,p是double*的同义词
  2. C++11规定了新的方法,使用别名声明using来定义类型的别名。
    using SI = Sales_item; // SI 是 Sales_item的同义词
  3. auto类型说明符 ,C++11引入auto,允许编译器通过初始值来推算变量的类型,所以 auto定义的变量必须有初始值。
    auto一般会忽略顶层const,而底层const则会保留,例如初始类型是常量整数,auto后就是整数,但初始类型是一个指针,则const属性会保留。即如果有顶层的const 则声明auto时,必须在auto前添加const,即const auto
  4. C++11引用decltype,来选择并返回操作数的数据类型,这个过程中,编译器分析表达式并推断它的类型,却不实际计算表达式的值。与auto不同,decltype返回变量的类型包括顶层const和引用在内。decltype的输入也可以是变量的地址,解引用,或是表达式,如果decltype给变量加了一层括号,则它就会被编译器认定为是表达式,变量是一种可以作为赋值语句左值的特殊表达式,因此这样的decltype会得到引用类型
    decltype(f()) sum = x; // sum的类型就是函数f的返回值类型
const int ci = 0, &cj = ci;
decltype(ci) x = 0; // x的类型为const int
decltype(cj) y = x; // y的类型是const int&
decltype(cj) z; // 错误,z是引用,必须初始化
int i = 42, *p = &i, &r = i;
decltype (r + 0) b; // 表达式加法的结果类型为int,故b是未初始化的int类型
decltype(*p) c; //错误,p本身是int类型,而*p,则是int&的引用类似,引用必须初始化。
decltype((i)) d; //错误,d是int&,必须初始化
decltype(i) e; // e 是未初始化的int

第3章:字符串、向量和数组

3.2string对象操作

  1. string对象可以使用< ,>,<=,>=,利用字符的字典序进行比较,且对字母的大小写敏感。
  2. 读写string对象:
    • 使用cin读取字符串时,string对象会自动忽略开头的空白(空格符、换行符、制表符),并从第一个真正的字符开始读起,直到遇见下一处空白开始。
    • 有时,我们希望能够在最终得到的字符串中保留输入的空白符,这时应该使用getline函数的参数是一个输入流和一个string对象,函数从给定的输入流中读入内容,直到遇到换行符为止,换行符也会被读进去,然后将读取的内容存入到那个string对象中去(不存在换行符)。getline遇到换行符就结束读取,哪怕一开始就是换行符,也是如此,那么所得结果是空字符串。
  3. size_type类型:所有用于存放string类的size函数返回值的变量都是string::size_type类型,它是一个无符号整数类型的值,并且能够存放任何string对象的大小。
  4. 可以将string对象与字面值相加,但不可以将同为字面值的两个对象相加,注意:字符串字面值与string是不同的类型。
string s;
cin >> s;
cout << s << endl;
string line;
// 每读入一整行
while(getline(cin, line))
	cout << line << endl; // 循环读入一整行,并打印	
  1. 处理string中的字符
    有如下常用库函数
    在这里插入图片描述

3.3vector

  1. 可以用花括号初始化vector的元素列表,注意花括号和括号的区别.
vector<string> v1{"a", "an", "the"}; // 列表初始化
vector<string> v2("a", "an", "the"); // 错误,只能用花括号初始化
vector<int> v1(10); // v1是10个0
vector<int> v2{10}; // v2是1个10
vector<int> v3(10, 1); // v3是10个1
vector<int> v4{10, 1}; // v4是1个10,1个1
  1. 迭代器 iterator,const_iterator,如果对象只需要读操作用const_iterator,对应的头迭代器和尾迭代器分别为cbegin()和cend()。
  2. 凡是使用了迭代器的循环体,都不要在循环体中向迭代器所属容器添加元素,这样操作会导致迭代器失效。

3.5数组

  1. 数组是一种复合类型,数组的声明中,数组元素的类型,以及数组中元素的个数也属于数组类型的一部分,因此数组中元素的个数必须在编译的时候是已知的,故只能使用常量表达式(字面值或者constexpr类型)来表示数组元素的个数。
unsigned cnt = 42;
constexpr unsigned sz = 42;
int arr[10]; // 10个整数的数组
int *parr[sz]; // 42个整型指针的数组,元素个数可以用常量表达式声明
string bad[cnt]; // 错误,cnt不是常量表达式
string strs[get_size()]; // 当get_size()返回值类型是constexpr时正确,否则错误。
  1. 数组不允许拷贝和赋值,即不能将数组的内容拷贝给其他数组作为其初始值,也不能用数组为其他数组赋值。
int a[] = {0,1,2};
int a2[] = a; // 错误,不能用一个数组初始化另一个数组
a2 = a; // 错误,不允许用一个数组直接赋值给另一个数组
  1. 理解复杂数组声明的含义,最好的办法是从数组的名字开始按照由内向外的顺序阅读。下边的例子3,首先*Parray说明它是一个指针,接下来观察右边,可知道Parray是个指向大小为10的数组的指针,最后观察左边,知道数组中的元素为int类型。例子4,&arrRef说明它是一个引用,观察左右可知,引用绑定的是一个大小为10的整型数组。例子5,&array,说明array是一个引用,观察左右可以知道,引用绑定的对象是一个大小为10的整型指针数组。
int *ptrs[10];
int &refs[10]; // 错误,引用非对象,不存在引用的数组
int (*Parray)[10] = &arr; // Parray指向一个含有10个整数的数组
int (&arrRef)[10] = arr; // arrRef引用一个含有10个整数的数组
int *(&arry)[10] = ptrs; // arry是数组的引用,该数组含有10个指针。
  1. 数组的元素也能使用范围for语句或者下标访问,使用数组下标通常将其定义为size_t类型,size_t是一种机器相关的无符号类型,它被设计的足够大以便能表示内存中任意对象的大小。这个size_t是与string::size_type, vector::size_type是一致的。
  2. 大多数表达式中,使用数组类型的对象其实是使用一个指向该数组首元素的指针,数组名其实是一个指向数组首地址的指针。
  3. C++11引用了begin()和end()函数,输入数组名,来获取指向数组头的指针和指向数组尾元素下一个位置的指针,使用begin()和end()可以很方便的循环遍历数组。
int arr[] = {1,2,3,4,5,6};
int *pbeg = begin(arr), *pend = end(arr);
while(pbeg != pend){
	pbeg++;
}
  1. 内置的下标运算符使用的索引值不是无符号类型,这一点与vector和string不同
int *p = &ia[2]; // p指向数组ia索引为2的元素
int j = p[1]; // j等价与 ia[2 + 1]
int k = p[-2]; // k等价与ia[2 - 2]

多维数组

  1. 严格讲,C++中没有多维数组,通常的多维数组其实就是数组的数组。C++11中引入了范围for循环,所以二维数组的遍历可以是以下两种。需要注意的是,第二种写法的时候,外层的row必须在auto加&引用修饰,不然将无法通过编译,这是因为不加引用修饰,则编译器会将row识别为指针,而非大小为4的数组。从这一点来讲,用范围for循环时,除了最内层外,其他所有层的控制变量都应该是引用类型
constexpr size_t rowCnt = 3, colCnt = 4;
int ia[rowCnt][colCnt];
for(size_t i = 0; i != rowCnt; i++){
	for(size_t j = 0; j != colCnt; j++){
		ia[i][j] = i*colCnt + j;
	}
}

size_t cnt = 0;
for(auto &row : ia){
	for(auto &col : row){
		col = cnt;
		++cnt;
	}
}
  1. 程序使用多维数组时,也会将多维数组的名字自动转换成指向数组首元素的指针。注意下边的第二行代码,圆括号()必不可少,否则*与int放一起,就有歧义,整型指针的数组。
int ia[3][4];
int (*p)[4] = ia; // p是一个指针,指向大小为4的整型数组(ia中的数组1)
p = &ia[2]; // p指向ia的尾元素

第4章:表达式

  1. 左值和右值:当一个对象被用作右值的时候,用的是对象的值(内容),当对象被用作左值的时候,用的是对象的(身份,在内存中的位置)。
  2. 布尔值不应该参与运算,对大多数运算符来说,布尔类型的运算对象被提升为int类型,例如下边的第二个例子,布尔值b为true,在运算时,被提升为int型的1,随后取负数,为-1,而-1再转为布尔值还是true。
  3. 对于复合表达式的书写,有以下两条经验准则:
    • 拿不准的时候最好用括号来强制让表达式的组会关系符合期望的程序逻辑。
    • 如果改变了某个运算对象的值,在表达式的其他地方不要再使用这个运算对象。这个规则有一个重要的例外,在改变运算对象的子表达式本身就是另一个子表达式的运算对象时,该规则无效。例如*++iterator, 递增运算符改变iterator的值,它又是解引用运算符的运算对象,因为在运算时的顺序是先递增,后解引用,所以这是一种常见的写法,不会造成问题。
int i = 1024;
int k = -i; // k是-1024
bool b = true;
bool b2 = -b; // 布尔值不参与运算,故b2还是true
  1. C++11规定商运算一律向0取整(即直接切除小数部分)。
  2. C++11规定除了-m导致溢出的特殊情况,其他时候(-m)/n和m/(-n)都等于-(m/n),
    m%(-n)等于m % n, (-m)%n等于-(m%n)。即符合相异的两个数相除,结果为两个数的绝对值相除后取负数,两个符合相异的数取模运算(取余),运算结果的绝对值为两个数的绝对值模运算,结果的符号则与第一个数的符号保持一致。
  3. 赋值运算满足右结合律:因此在出现连等情况时,最好从右向左读来理解。例如下边的第二行,右侧表达式为jval = 0;表达式的返回结果为jval,故会将jval赋值给ival。对于多重赋值语句中的每一个对象,它的类型或者与右边对象的类型相同,或者可由右边对象的类型转换得到。
int ival, jval;
ival = jval = 0; // 正确:都被赋值为0
int kval, *pval;
kval = pval = 0; //错误,不能把指针的值赋值给int
string s1, s2;
s1 = s2 = "OK"; // 字面值可以转为string对象
  1. 递增递减运算符:在下边的例子中,*peg++实际上等价于 ✳(peg++),peg++把pbeg的值加1,然后返回pbeg的初始值的副本作为求值结果,因此解引用的是当前pbeg指针指向的结果,但这条语句结束后,pbeg已经递增了。注:后置递增运算符的优先级高于解引用运算符
auto pbeg = v.begin();
// 输出元素直至遇到第一个负数为止
while(pbeg != v.end() && *beg >= 0){
	cout << *pbeg++ << endl; // 输出当前的值,并将pbeg向前移动一个元素
}
  1. 嵌套条件运算符:允许在条件运算符的内部嵌套另外一个条件运算符,即条件表达式可以作为另一个条件运算符的condition或expr。例如下边这个例子将成绩分为三档,high pass,pass,fail:
finalgrade = (grade > 90) ? "high pass" : (grade < 60) ? "fail" : "pass";
  1. 移位运算符满足左结合律,它的优先级不高不低,介于中间:比算术运算符的优先级低,但比关系运算符、赋值运算符的优先级高。因此在一次使用多个运算符时,有必要在适当的地方加上括号。
cout << "hi" << "there" <<endl;
((cout << "hi") << "there") << endl; //  << 满足左结合律,上边的写法等价这一行
cout << 42 + 10;  // 正确,输出52
cout << (10 < 42); // 正确,输入1
cout << 10 < 42; // 错误,<< 运算符优先级比 < 高,这句话试图比较cout 和42
  1. sizeof运算符:返回一条表达式或是一个类型名字所占的字节数,所得的值是一个size_t类型的常量表达式。sizeof运算符的结果部分地依赖其作用的类型:
    • 对char或者类型为char的表达式执行sizeof结果得1;
    • 对引用类型执行sizeof运算得到被引用对象所占空间的大小;
    • 对指针执行sizeof运算得到指针本身所占空间的大小;
    • 对解引用执行sizeof运算,得到指针所指对象所占空间的大小,且指针不需有效,即sizeof并不会真正解引用指针。

第5章:语句

  1. 范围for语句:从下面范围for语句的实际写法可以看出,它是利用了变量的迭代器进行遍历的,而采用迭代器进行遍历是不能在循环中向vector中增加或者删除元素的,故不能通过范围for语句增加或者删除容器中的元素。
vector<int> v = {0,1,2,3,4,5,6,7,8,9};
for(auto &r : v){
	r*=2;
}
// 范围for语句等价于传统的for语句的形式为:
for(auto beg = v.begin(), end = v.end(); beg != end; ++beg){
	auto &r = *beg; // r必须是引用类型才能对元素执行写操作
	r *= 2;
}
  1. try语句块和异常处理
    异常处理包括:
  • throw表达式:使用throw表达式表示遇到了无法处理的问题,即throw引发(raise)了异常。
  • try语句块,异常处理部分使用try语句块处理异常,try语句块以关键字try开始,并且以一个或者多个catch子句结束,try语句块中代码抛出的异常通常会被某个catch子句处理。因此catch子句也被称为异常处理代码。
  • 一套异常类,用于在throw表达式和相关的catch子句之间传递异常的具体信息。
    下边这个例子实现:
    // 把两个Sales_item对象相加,程序检查它读入的记录是否是同一种数据,如果不是输入一条消息退出
Sales_item item1, itme2;
cin >> item1 >> item2;
if(itme1.isbn() == itme2.isbn()){
	cout << item1 + item2 << endl;
	return 0; //表示成功
}else{
	return -1; // 表示失败
}
 // 在真实的程序中,应该把对象相加的代码和用户交互的代码分离开,下边是抛出异常的写法:
 if(item1.isbn() != itme2.isbn())
 	throw runtime_error("Data must refer to same ISBN!");

类型runtime_error是标准库异常类型的一种。抛出异常将终止当前的函数,并把控制权转移给能处理该异常的代码。

下边书写一个处理异常的代码:

while(cin >> item1 >> item2){
	try{
		// 执行添加两个Sales_item对象的代码
		// 如果添加失败,代码抛出一个runtime_error异常
	}catch(runtime_error err){
		// 提醒用户两个ISBN必须一致,询问是否重新输入
		cout << err.what()
		 	 << "\nTry Again? Enter y or n" << endl;
		char c;
		cin >> c;
		if (!cin || c == 'n'){
			break; // 跳出while循环
		} 	 
	}
}

在这里插入图片描述

第6章:函数

6.1-6.2函数的参数传递

  1. 局部静态对象(使用static修饰的类型)在程序的执行路径第一次经过对象定义语句的时候初始化,并且直到程序终止才被销毁,在此期间,即使对象所在的函数结束执行也不会对它有影响。
    例如下边这个函数会统计它自己被调用了多少次。
size_t count_calls(){
	static size_t ctr = 0; // 调用结束后,这个值仍然有效
	return ++ctr;
}

int main(){
	for(size_t i = 0; i != 10; ++i){
		cout << count_calls() << endl;
	}
}
  1. 如果函数需要修改形参的值,最好使用引用,以避免拷贝的开销,如果函数不需要修改形参的值,最好声明为常量引用。
  2. 使用引用形参返回额外信息:一个函数只能返回一个值,然而有时我们需要同时返回多个值,引用形参为我们一次返回多个结果,提供了有效的途径。
  3. const形参和实参:当用const实参初始化形参时会忽略掉顶层的const。当形参有const修饰时,传给他常量对象或者非常量对象都可以。在C++中允许定义若干相同名字的函数,前提是不同函数的形参列表应该有明显的区别,因为顶层的const被忽略了,所以下边这个例子中,传入两个fcn函数的参数可以是完全一样的,因此尽管两函数的形参有所差异,但实际上两者等价,这样重载是错误的。
void fcn(const int i){};
void fcn(int i){}; //错误:忽略顶层const, 重定义fcn
  1. 应尽量使用常量引用,把函数不会改变形参定义为普通引用是是一种比较常量的错误,首先这样给调用者一种误导,即函数可以修改它实参的值,此外非常量引用还会现在函数所能接受的实参类型。比如我们调用函数时经常会使用字面值,而使用非常量引用是无法接受字面值对象的。
  2. 数组形参,数组由于不允许拷贝,以及在使用是数组名通常会被转换为首元素的指针。因为不能拷贝数组,所以不可以值传递的方式使用数组参数,因为数组会被转换为指针,所以我们在为函数传递一个数组时,实际上传递的指向数组首元素的指针。
// 尽管形式不同,但这三个print函数等价
// 每个函数都有一个const int*类型的形参
void print(const int*);
void print(const int[]);// 函数的意图是传入数组,但会被转为首元素指针,故实际上与上一行是等价的
void print(const int[10]);// 维度是期望的数组含有的元素个数,实际上传入的却不一定。
  1. 因为数组是以指针的形式传入形参的,故函数不知道数组的具体尺寸,有三种管理指针形参的信息。
    • 使用标记指定数组的长度(例如C风格的字符串在最后一个字符后边跟着一个空字符来标记,函数在处理到空字符的时候就停止了。)
    • 使用标准库规范,管理数组实参的第二种技术是传递指向数组首元素和尾元素的指针,这种技术受到了标准库的启发。
    • 显式传递一个表示数组大小的形参。
  2. 数组引用形参,C++语言允许将变量定义为数组的引用,因此形参也可以是数组的引用。此时形参应该绑定到实参(数组)上。
void print(int (&arr) [10]){
	for(auto elem : arr){
		cout<< elem << endl;
	}
}

在数组的章节(3.5.1)中,对于复杂数组的声明应该从数组名字开始,由内向外(数组名向两边)来理解。 &arr,说明arr是一个引用,再看两边知道,arr绑定的是一个大小为10的整型数组。注意&arr两端的括号必不可少,否则有歧义。另外,由于维度属于数组声明的一部分,因此print()函数被限制只能接受大小为10的整型数组作为实参。

f(int &arr[10]) // 错误:将arr声明为了引用的数组,&与int共同构成了数组元素的类型
f(int (&arr)[10]) // 正确,arr是具有10个整型元素的数组的引用
  1. 传递多维数组:C++实际上没有多维数组,多维数组实际上是数组的数组,当多维数组传递给函数时,实际传递的是数组首元素的指针。多维数组实际上可以理解为多个指针元素构成的数组。
  2. main函数处理命令行选项。
    有时,需要给main传递实参,一种常见的情况是用户通过设置一组选项来确定函数所要执行的操作,例如假定main函数位于prog之内,我们向程序传递下边的选项:
    prog -d -o ofile data0
    int main(int argc, char *argv[]){……}
    第一个形参argc表示数组中字符串的数量,第二个形参是数组,是C风格指向字符串的指针。当实参传递给main函数之后,argv的第一个元素指向程序的名字或者一个空字符串,接下来的元素依次传递命令行提供的实参,最后一个指针之后的元素值保证为0.
    以上边提供的命令行为例:
    argc应该等于5,argv应该包含如下的C风格字符串,其中argv的头元素为程序名子,尾元素为0,实际上尾元素没有计入argc中。读取argv实参时,实际有效的用户输入参数从argv[1]开始。
argv[0] =  "prog";
argv[1] = "-d";
argv[2] = "-o";
argv[3] = "ofile";
argv[4] = "data0";
argv[5] = 0;
  1. 含有可变形参的函数:
    * 当要传递的实参数目不定,但是类型相同时,可以传递一个名为initializer_list的标准库类型;
    * 当要传递的实参类型也不同时,需要编写可变参数模板来实现这个函数。
initializer_list<T> lst; // 默认初始化T类型元素的空列表
initializer_list<T>lst{a,b,c……}; // 使用花括号初始化成员列表
lst2(lst);
lst2 = lst; // 拷贝或者赋值一个initializer_list对象不会拷贝列表中元素,原始列表和副本是共享元素的。
lst.size();
lst.begin();
lst.end();
  1. initializer_list的使用:
    initializer_list用法大多数时候与vector是一致的,不同的是initializer_list对象中的元素永远是不可更改的常量。
void error_msg(ErrCode e, initializer_list<string> il)
{
	cout << e.msg() << ": ";
	for (const auto &elem : il) // 既然initializer_list中是不可改变的常量元素,那么就应该使用常量引用来接收元素
		cout << elem << " " ;
	cout << endl;
}

// 调用error_msg
if(expected != actual)
	error_msg(ErrCode(42), {"functionx",expected , actual});
else 
	error_msg(ErrCode(0), {"functionX", "okay"});	

6.3 返回类型与return语句

  1. 不要返回局部对象的引用或指针:函数完成后,它所占的存储空间将被释放,因此,函数终止意味着函数内定义的局部变量的引用和指针将不再指向有效的内存区域。
  2. 引用返回左值:函数的返回类型决定函数调用是否是左值,调用一个返回引用的函数得到左值,其他返回类型得到右值。可以像其他左值那样来使用返回引用的函数的调用,特别是,我们能为返回类型是非常量引用的函数的结果赋值。
char &get_val(string &str, string::size_type ix){
	return str[ix];
}

int main(){
	string s("a value");
	cout << s << endl; // a value
	get_val(s, 0) = 'A'; // 将s[0]的值改为A
	cout << s << endl; // A value
}
  1. 列表初始化返回值:C++11规定函数可以返回花括号包围的值的列表。
vector<int> smallTow(vector<int> arr){
	sort(arr.begin(), arr.end());
	return {arr[0], arr[1]};
}
  1. 返回数组指针:数组不能被拷贝,故只能返回数组的指针。
    int (*func(int i))[10];
    func(int i)表示函数func需传入一个int型的实参
    int (*func(int i))[10]表明函数返回返回的是一个int型的数组的指针

上边的表达可以使用C++11标准的尾置返回类型。尾置返回类型跟在形参列表后边,并以一个->符号开头,本该出现返回值类型的地方使用auto代替。
auto func(int i) -> int (*)[10];
func 返回一个指针,该指针指向含有10个整数的数组。
使用->指向的位置返回类型,表达更加清晰明了。

6.5内联函数和constexpr函数

  1. 一次函数调用包含一系列工作:调用前保存寄存器,返回时恢复,可能需要拷贝实参,程序转向另一个新的位置执行等。将函数指定为内联函数可以避免函数调用时的开销,通常内联函数会在它每个调用点上“内联地”展开,例如我们把shortString函数定义为内联函数,它实际的调用方式为:
    cout << shortString(s1, s2) << endl;
    编译过程中会展开成:
    cout << (s1.size() < s2.size () ? s1 : s2) << endl;
    从而消除shortString函数运行时的开销。
    在shortString函数的返回类型前边加上关键字inline,就可以将它声明为内联函数:
inline const string & shortString(const string &s1, const string &s2){
	return s1.size() <= s2.size() ? s1 : s2;		
}

一般来说,内联机制用于优化规模较小,流程直接,频繁调用的函数。
2. constexpr函数:能用于常量表达式的函数。定义constexpr函数与普通函数类似,不过要遵循如下规定:

  • 数的返回类型及所有形参都得是字面值类型
  • 函数体中有且只有一条return语句

6.7 函数指针

  1. 函数指针指向的是函数而非对象,函数指针指向某种特定类型的函数,函数的类型由函数的返回类型和形参类型共同决定,与函数名无关。
bool lengthCompare(const string &, const string &);

该函数的类型是bool(const string &, const string &)。想声明一个可以指向该函数的指针,只需要用指针替换函数名即可:注pf是指针名,两边的括号必不可少

bool (*pf) (const string &, const string &); // 未初始化
  1. 使用函数指针,当我们把函数名作为一个值使用时,该函数自动转为指针(即取址符号是可选的);此外我们能直接使用指向函数的指针调用该函数,无须解引用指针;不同函数类型的指针不存在转换关系,但都可以用nullptr或0初始化,来表示指针没有指向任何一个函数。
pf = lengthCompare; // pf指向名为lengthCompare的函数
pf = &lengthCompare; // 等价的赋值语句,即取址符号是可选的
bool b1 = pf("hello","goodbye"); // 无须解引用pf直接调用
bool b2 = (*pf)("hello", "goodbye"); //一个等价的调用
bool b3 = lengthCompare("hello", "goodbye"); // 另一个等价的调用
  1. 函数指针形参:与数组类似,虽然不能定义函数类型的形参,但是形参可以是指向函数的指针;我们也可以直接把函数当做实参使用,它会自动转为函数指针;有时直接使用函数指针类型显得冗长繁琐,类型别名和decltype能让我们简化使用了函数指针的代码。
// 第三个形参是&
void useBigger(const string &s1, const &s2, bool pf(const string &, const string &)); 
// 等价的声明
void useBigger(const string &s1, const string & s2, bool (*pf)(const string &, const string &));
// 等价的声明
void useBigger(const string &s1, const string & s2, lengthCompare);

// Func 和 Func2是函数类型
typedef bool Func(const string&, const string&); // 类型别名声明
typedef bool decltype(lengthCompare) Func2; // 等价的类型
// Funcp 和Funcp2是指向函数的指针
typedef bool(*Func)(const string&, const string&);
typedef decltype(lengthCompare) *FuncP2;

// 使用类型别名重新声明useBigger,虽然这里传入的是函数,但编译器会自动把他转为函数指针
void useBigger(const string &s1, const &s2, Func);
void useBigger(const string &s1, const &s2, Func2);
  1. 返回指向函数的指针:函数作为形参时,编译器会将函数自动转为函数指针,但是返回函数指针时,必须把返回类型写成指针的形式。想要声明一个返回函数指针的函数,最简单的办法是使用类型别名。如果不使用类型别名,那么等价的声明是:
    int (*f1(int))(int *, int);
    按照由内到外的规则,f1首先有形参列表故是一个函数,前面有*,说明是返回一个指针,再向两边,确定返回的函数指针指向的函数的形式,当然也可以使用尾置返回类型的方式:
    auto f1(int) -> int (*)(int*, int);
    如果明确知道返回的函数是哪一个,也可以使用decltype来获取函数的类型,但需要注意的是,decltype返回函数类型,而非函数指针类型,因此下边最后一行的(*)必不可少。
using F = int(int*, int); // F是函数类型
using PF = int(*)(int*, int); // PF是指针类型

PF = f1(int); // PF是指向函数的指针,f1返回函数的指针
F f1(int); // 错误,F是函数类型,不可返回函数,只能返回函数指针
F *f1(int); // 正确,显示地指向返回类型是指向函数的指针。
int (*f1(int))(int *, int);
auto f1(int) -> int (*)(int *, int);
decltype(sumLength) *getFcn(const string &);

第7章:类

  1. 构造函数初始值列表:看下边这两个构造函数的例子,给构造函数声明了三个参数,然后冒号与花括号之间,将这三个参数赋值给类中的对象,最后的花括号定义为空的函数体,这就是构造函数初始值列表。
Sales_data(const std :: string &s) : bookNo(s){}
Sale_data(const std :: string &s, unsigned n, double p) :bookNo(s), units_sold(n), revenue(p*n){}
TreeNode(int x):val(x), left(nullptr), right(nullptr){}
TreeNode(int x, TreeNode* _left, TreeNode* _right) : val(x), left(_left), right(_right){}
  1. 使用关键字mutable修饰可变数据成员,这样的成员变量,即是在常函数中也可以修改它的值。
  2. 当我们提供一个类内初始值时,必须以等号“=”或者花括号表示
    下边的例子screens用花括号初始化,size初始化为1.
class Window_mgr{
	private:
	// 这个window_mgr追踪的screen
	// 默认情况下,一个window_mgr包含一个标准尺寸的Screen
	std::vector<Screen> screens{Screen(24, 80, ' ')};
	size = 1;
}
  1. 返回*this的成员函数:下边的两个set函数,返回的是※this,即类对象本身,且函数声明为引用形式,返回引用的函数是左值的,意味着这些函数返回的是对象本身而非对象的副本。如果我们定义的返回类型不是引用,则返回的是右值,只能改变临时副本的值,而非对象本身的值。
class Screen{
	public:
	Screen &set(char);
	Screen &set(pos, pos, char);
};
inline Screen &Screen::set(char c){
	contents[cursor] = c;
	return *this;
}
inline Screen &Screen::set(pos r, pos col, char ch){
	contents[r*width + col] = ch;
	return *this;
}

// 把光标移动到指定位置,设置该位置的字符值
myScreen.move(4, 0).set('#');

// 上边的一条语句等价于下边两条语句。
myScreen.move(4.0);
myScreen.set('#');
  1. 从const成员函数(常函数)返回*this
    假如为screen添加一个display的函数,负责打印screen的内容,那么它不需要改变screen的内容,因此我们令display为一个const成员,此时this将是指向const的指针,而※this是const对象,如果真的令distplay返回一个const的引用,则我们将不能把display嵌入到一组动作的序列中。这时需要基于const的重载:通过区分成员函数是否是const的,我们可以对其进行重载。
class Screen{
	public:
	// 根据对象是否是const重载display函数
	Screen &display(std::ostream &os){do_display(os); return *this;}
	const Screen &display(std::ostream &os) const{os << contents;}
	private:
	// 该函数负责显示screen的内容
	void do_display(std::ostream &os) const {os << contents;}
	// 其他成员与之前的版本一致
}

当一个成员调用另一个成员时,this指针在其中隐式的传递。当display调用do_display时,它的this指针隐式传递给do_display。而当display的非常量版调用do_display时,它的this指针将隐式地从指向非常量的指针转换成指向常量的指针。
当do_display完成时,display函数各自返回解引用的this所得的对象,在非常量版本中this指向一个非常量对象,返回普通引用,而const成员返回一个常量引用。

Screen myScreen(5,3);
const Screen blank(5,3);
myScreen.set('#').display(cout);
blank.display(cout);
  1. 构造函数的初始值有时必不可少,在成员属于引用或者常量类型时,由于引用和常量必须在定义的时候初始化,所有类成员中有这两种类型但只有声明,没有在构造函数中初始化的写法是错误的。
class ConstRef{
public:
	ConstRef(int ii);
private:
	int i;
	const int ci;
	int &ri;	
};

// 默认初始化
ConstRef::ConstRef(int ii){
	i = ii;  // 正确
	ci = ii; // 错误,不可给const赋值
	ri = i; //错误,ri没被初始化
}

// 正确的构造函数的格式是:
ConstRef::ConstRef(int ii): i(ii), ci(ii), ri(i) {}
  1. 成员初始化的顺序:成员初始化的顺序与它们在类中定义出现的顺序是一致的。构造函数初始值列表中的顺序不会影响实际的初始化顺序,因此一般来讲,最好令构造函数初始值的顺序与成员声明时的顺序一致,并且尽量避免使用某些成员初始化其他成员。
  2. 委托构造函数:一个委托构造函数使用它所属类的其他构造函数执行它自己的初始化过程,或者说是它把自己的一些职责委托给了其他构造函数。
class Sales_data{
public:
	// 非委托构造函数使用对应的实参初始化成员
	Sales_data(std::string s, unsigned cnt, double price): bookNo(s), units_sold(cnt), revenue(cnt*price){}
	// 其余构造函数全部委托另一个构造函数
	Sales_data():Sales_data("", 0, 0){}
	Sales_data(std::string s):Sales_data(s, 0, 0){}
	Sales_data(std::istream &is):Sales_data(){read(is, *this);}
};
  1. explicit构造函数:explicit关键字通常用来将构造函数标记为显式类型转换,即在创建对象的时候不能进行隐式类型转换。
    • explicit构造函数只能用于直接初始化。
    • 它仅对一个实参的构造函数有效,需要多个实参的构造函数不能用于执行隐式转换。
    • 只能在类内声明构造函数时使用explicit关键字,在类外部定义时不应重复。
class Sales_data{
	public:
	Sales_data() = default;
	Sales_data(const std::string &s, unsigned n, double p):bookNo(s), units_sold(n), revenue(p*n) {}
	explicit Sales_data(const std::string &s):bookNo(s)
	explicit Sales_data(std::istream&);
};

// 错误,explicit关键字只允许出现在类内构造函数声明处
explicit Sales_data::Sales_data(istream& is){
	read(is, *this);
}

Sales_data item1(null_book); //正确,直接初始化
Sales_data itme2 = null_book; // 错误,不能将explicit构造函数用于拷贝形式的初始化
  1. 聚合类:使得用户可以直接访问其成员,并且具有特殊的初始化语法形式,当一个类满足如下条件,我们说它是聚合的:
    • 所有成员都是public的
    • 没有定义任何构造函数
    • 没有类内初始值
    • 没有基类,也没有virtual函数
struct Data{
	int ival;
	string s;
};

Data vall = {0, "Anna"}; // 注意初始化的顺序要与声明的顺序一致

  1. 字面值常量类:数据成员都是字面值类型的聚合类是字面值常量类,字面值类型的类可能含有constexpr函数成员,constexpr函数的参数和返回值必须是字面值类型。如果一个类不是聚合类,如果符合以下要求也是字面值常量类:

    • 数据成员都是字面值类型
    • 类必须至少含有一个constexpr构造函数
    • 如果一个数据成员含有类内初始值,则内置类型成员的初始值必须是一条常量表达式;如果成员属于某种类型,则初始值必须使用成员自己的constexpr构造函数。
    • 类必须使用析构函数的默认定义,析构函数负责销毁类的对象。
  2. constexpr构造函数,constexpr构造函数必须初始化所有数据成员,初始值使用constexpr构造函数或者是一条常量表达式。

class Debug{
public:
	constexpr Debug(bool b = true):hw(b), io(b), other(b){}
	constexpr Debug(bool h, bool i, bool o):hw(h), io(i), other(o){}
	constexpr bool any(){return hw || io || other}
private:
	bool hw; // hardware error
	bool io; // io error
	bool other; // other error
}
  1. 类的静态成员。
    • 静态成员函数和静态成员不与对象绑定在一起不包含this指针,所有对象的静态成员共享一份内存数据
    • 静态成员函数不能声明为constconst用于后置修饰函数时只用于限定成员函数,意味着将被修饰的成员函数的隐式参数——this指针由原来的Class*const变为constClass※const类型,使得在该成员函数内不能修改成员属性,除非该属性被mutable修饰。而static类函数并没有隐式的this指针,因为其本质上还是属于C函数——满足__cdecl调用协定。而成员函数被称为__thiscall,带有隐式的this指针参数。
    • 虽然静态成员不属于类的某个对象,但任然可以使用类的对象、指针或引用来访问静态成员。
    • 和其他成员函数一样,既可以在类内定义静态成员函数也可以在类外指明所属类的情况下,定义成员函数,但需要注意static关键字只出现在类内部的声明语句中
    • 由于静态成员不属于类的任何一个对象,故它们并不是在创建类的对象时被定义的,意味着它们不是由类的构造函数初始化。一般而言,我们必须在类的外部定义和初始化每个静态成员,不能在类的内部初始化静态成员。

琐碎

cin cout clog cerr
/*注释不可嵌套
使用//单行注释
unsigned默认是unsigned int
可寻址的最小内存块称为“字节”8bit

存储的基本单元为“字”4/8 Byte

给无符号类型一个超过它范围的值结果是该值对无符号类型表示数值总数取模所得余数。
例如
unsigned char c =-1; // 假设char占8位,值为255(-1 mod 256)

不允许给非常量引用赋值常量对象
但允许给一个常量引用绑定非常量的对象,字面值或者一般表达式。
常量引用绑定非常量是允许的,不允许通过常量引用修改值,但被绑定的对象本身不受影响,可以赋值或者通过其他引用来修改。

指向常量的指针或者引用,是他们“自以为是”地认为自己指向了常量,便自觉不去修改所指对象的值,实际上所指对象可能并不受修改的约束。

顶层const表示指针本身是个常量,(指针常量),底层const表示指针所指对象是个常量,(常量指针)。想象一个自上指下的指针,顶层常量是指针本身是常量,底层常量是所指对象是常量。

常量表达式:值不会改变且在编译过程中就能得到计算结果的表达式。

constexpr变量
C++11允许将变量声明为constexpr类型以便于由编译器来验证变量值是否是常量表达式。在新标准下可以定义一种特殊的constexpr函数去初始化一个constexpr变量。

typedef double wages; //给double赋别名wages
C++11规定了一种新的别名声明来定义类型的别名:
using SI =Sales_item;

别名类型使用时,通常将别名替换为原始名称来理解,但对于复合类型别名需要注意,eg:
typedef char *pstring;
这里的意思是给char *起别名为pstring
const pstring cstr = 0; //cstr是指向char的常量指针
若直接替换,则为:
const char *cstr = 0; //这样cstr是一个指向const char的指针。
暂时理解是,原本类型是char指针,const修饰指针,直接替换则变为const修饰char。

auto类型说明符
C++11引入auto,允许编译器通过初始值来推算变量的类型,所以 auto定义的变量必须有初始值。
auto一般会忽略顶层const,而底层const则会保留,例如初始类型是常量整数,auto后就是整数,但初始类型是一个指针,则const属性会保留。

decltype类型指示符
作用是选择并返回操作数的数据类型,在此过程中,编译器分析表达式并得到它的类型,但不实际计算表达式的值。
decltype (f()) sum = x;//sum的类型是函数f的返回类型。

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值