精读《C++ primer》学习笔记(第一至三章)
第一章:
重要知识点:
类型:一种类型不仅定义了数据元素的内容,还定义了这类数据上可以进行的运算;所以说类定义,实际上就是定义了一种数据类型;
>>
和<<
运算符返回其左侧的运算对象:
std::cin >> v1 >> v2;
和以下代码执行结果一样:
std:cin >> v1;
std:cin >> v2;
良好的行注释风格:
注释内的每一行都以一个星号开头,从而指出整个范围都是多行注释的一部分;
#include<iostream>
using namespace std;
/*
* 简单主函数:
* 读取两个数,求它们的和
*/
int main() {
int v1 = 0, v2 = 0;
cin >> v1 >> v2;
cout << v1 + v2 << endl;
return 0;
}
endl
是操纵符(manipulator),执行效果是结束当前行,并将与设备关联的缓冲区中的内容刷到设备中,刷新操作可以保证到目前为止程序所产生的所有输出都真正写入输出流中,而不是仅停留在内存中等待写入流。
读写缓冲区的动作与程序中的动作是无关的,读cin
时会刷新cout
,cerr
和clog
关联到标准错误,写到cerr
的数据是不缓冲的,写到clog
的是被缓冲的。
读取数量不定的输入数据:
int value = 0;
while(cin >> value)
当使用一个istream
对象作为条件时,其效果是检测流的状态,如果遇到文件结束符,或者遇到无效的输入时(例如输入的值不是一个整数),istream
对象的状态会变为无效。
using声明,举例:
using std::cin;
using指示,举例:
using namespace std;
文件结束符:
Windows: Ctrl + Z
, 再按Enter
或者Return
Unix: Ctrl + D
编译:
常见的编译错误有:语法错误、类型错误、声明错误
单个错误通常具有传递效应,导致编译器在其后报的错误比实际数量多得多,一个好的习惯是,每修正一个,或者一小部分错误后就立即编译代码。即所谓的“编辑-编译-调试”周期。
好的代码风格:
对于作为函数界定符的花括号,习惯放在单独的一行中,
对于流程控制语句中的花括号,通常放在关键字后,
对于复合IO表达式设置缩进,可以使输入输出运算符排列整齐。
头文件:
使类或者其他名字的定义可被多个程序使用的机制,可以用头文件来访问为自己的应用程序所定义的类,习惯上,头文件用类的名字来命名,习惯上用.h
,标准库文件通常不带后缀,编译器一般不关心文件名的形式。
标准库中的头文件用< >
来包围,其他头文件则用" "
来包围。
类变量的初始化:
类类型的变量如果未指定初值,则按照类定义指定的方式初始化,而定义在函数内部的内置类型变量默认是不初始化的,除非有显式初始化语句。
文件重定向:
$ your_program <infile >outfile
上述程序会从一个infile的文件中读取数据,然后将结果写入一个outfile的文件中。
调用运算符:
( )
为调用运算符,里面放置实参列表:
item1.isbn()
第二章:
重要知识点:
2.1 基本内置类型
内置类型的实现与机器硬件密切相关。
整型:
也包含字符和布尔类型在内,之前不常用的有wchar_t
, char16_t
, char32_t
。wchar_t
可以存放机器最大扩展字符集中的任意一个字符, char16_t
和char32_t
为Unicode
字符集服务。
除布尔类型和扩展的字符类型外,其他整型可以分为signed
和unsigned
类型。未明确指明,char
会表现为上述中的一种,具体由编译器决定。
常用的范围概念:
short
可以存放五位十进制数;
long
可以存放十位十进制数;
long long
可以存放十九位十进制数。
字节:可寻址的最小内存块,
字:存储的基本单元。
一些编程的经验准则:
- 当确知数值不为负时,使用无符号类型;
long
一般和int
有一样的尺寸,如果数值超过了int
的范围,选用long long
;- 在算术表达式中不要使用
char
或bool
,除非明确指明是unsigned char
还是signed char
; - 执行浮点运算时选用
double
,因为float
精度不够,而long double
提供的精度一般不必要,且开销太大。 - 程序应当避免依赖与实现环境的行为,比如当把
int
类型的尺寸看成是不变的,那么这样的程序将是不可移植的。
当一个表达式中既有unsigned
也有int
时,int
类型的数会转换为无符号数。
一个错误示例:
for (unsigned u = 10; u >= 0; --u)
cout << u << endl;
字面值:
short
没有对应的字面值;
nullptr
是指针字面值;
尽管整型字面值可以存储在带符号的数据类型中,但严格来说,字面值并不会是负值,比如-32,那个负号并不在字面值内,只是表示对字面值取负值。
如果两个字符串字面值位置相邻且仅由空白符分隔,则它们实际上是一个整体。比如:
cout << "a really, really long string literal "
"that spans two lines" << endl;
Notes: 使用长整型字面值时,用L
来标记,因为l
和1
很容易混淆。
泛化的转义字符只能用十六进制和八进制表示,如\x4d
, \115
。
C++中,“变量”和“对象”可以互换使用。
2.2 变量
初始化和赋值是不同的操作,初始化是创建变量时赋予一个初始值,而赋值的含义是把对象的当前值擦除,用新值代替。
初始化的方式:
int i = 0;
int i = {0};
int i(0);
int i{0};
用花括号的是列表初始化,如果初始值存在丢失信息的风险时,编译器将报错:
long double ld = 3.1415926;
int a{ld}, b = {ld}; // 错误
int c(ld), d = ld; // 正确
ld
的小数部分会丢失,而且int
也存不下ld
的整数部分。
默认初始化:
内置类型如果定义于任何函数体之外,那么将被初始化为0
,函数体中不被初始化;每个类决定其初始化对象的方式,string
类支持无须显示初始化而定义对象,默认为空串。
编程Tips:初始化每一个内置类型的变量。
声明规定了变量的类型和名字,定义还申请了存储空间,可以为变量赋一个初试值,任何包含了显式初始化的声明即成为定义。在函数体内,试图初始化一个由extern
关键字标记的变量将引发错误。声明而不定义:
extern int i;
变量只能定义一次,但可以多次声明。
在多个文件中使用同一个变量时,变量的定义只能出现在一个文件中,而其他用到该变量的文件必须声明,而不能定义。
标识符:
- 用户自定义的标识符不能连续出现两个下划线;
- 不能以下划线紧连大写字母开头;
- 定义在函数体外的标识符不能以下划线开头。
全局作用域没有名字,内层作用域中显式地访问全局变量,如下:
cout << ::variable;
2.3 复合类型
左值引用:
必须被初始化,而且之后无法再重新绑定到另外一个对象上,引用不是一个对象,所以不能定义引用的引用。常见错误:
int &refVal = 10; // 错误:引用类型的初始化必须是一个对象
double dval = 3.14;
int &refVal2 = dval; // 错误:引用类型初始化必须为int类型
指针本身就是一个对象,允许对指针赋值和拷贝,不能定义指向引用的指针。空指针的生成方法:
int *p1 = nullptr;
int *p2 = 0;
int *p3 = NULL; // NULL是预处理变量,需要#include cstdlib
但不要把int型的变量直接赋给指针:
int zero = 0;
pi = zero;
Notes: 在定义了对象之后再定义它的指针,建议初始化所有的指针。
void*
是一种特殊类型的指针,以void*
的视角来看内存空间仅仅只是内存空间,无法访问内存空间中所存的对象。
指向指针的引用:
int *p, *&r = p;
从右往左读,先得知r
是一个引用,然后确定是指向int型指针的引用。
2.4 const限定符
const int bufSize = 512;
bufSize = 512; // 错误,不能对const对象写值
const
对象必须被初始化
int i = 42;
const int ci = i; // 正确
int j = ci; // 正确
拷贝完成后,新对象和原来的对象没有关系了。
const对象仅在文件内有效,编译器将把用到该变量的地方都替换成对应的值。
为了能够像其他变量一样在多个文件中声明并使用,需要在const
变量声明和定义时添加extern
关键字。
const
的引用:
准确来说,并不存在“常量引用”这一说法,因为引用不是对象,无法让引用本身保持不变,而语言要求引用不能更改所绑定的对象,所以所有的引用又都算是常量。
引用类型应该和所引用对象保持一致,但有例外情况:
初始化常量引用时允许用任意表达式作为初始值,只要它能转换成引用的类型。
double dval = 3.14;
const int &ri = dval;
详细的编译代码类似如下:
const int temp = dval;
const int &ri = temp;
ri
绑定了一个临时量对象,如果ri
不是常量,那么将无法通过ri
更改dval
的值,C++把这种行为归为非法。
对const
的引用可能引用一个并非const
的对象:
int i = 42;
int &r1 = i;
const int &r2 = i;
r1 = 0; // 正确
r2 = 0; // 错误
常量引用所绑定的对象可以通过其他引用来修改。
指向常量的指针不能用来改变其所指对象的值。
要想存放常量对象的地址,只能使用指向常量的指针。
允许一个指向常量的指针指向一个非常量的对象,如下:
const double pi = 3.14;
const double *cptr = π
double dval = 3.14;
cptr = &dval;
常量指针必须初始化,而且初始化后值不能更改。
顶层const
:对象本身是一个常量;
底层const
:指针或者引用所指的对象是一个常量。
当执行拷贝时,底层const
必须要有相当的资格,一般是非常量可以转化为常量,而顶层const
不受影响,当做一般的数据类型来看待就可以了,因为拷贝不会改变拷贝对象的值。
对于指针而言,*
在const
之前,则为顶层,*
在const
之后,则为底层。
常量表达式是值不会改变,在编译时就得到了计算结果的表达式,一个对象是不是常量表达式,由它的类型和初始值共同决定:
int staff_size = 27; // 不是
const int sz = get_size(); // 不是
允许使用constexpr
来让编译器验证变量是不是常量表达式,除了constexpr
函数外,普通函数不能作为constexpr
变量的初始值。
声明constexpr
时用到的类型必须是字面值类型,算术类型,引用,指针都是字面值类型,一个constexpr
指针初始值必须是nullptr或0,或某个固定地址中的对象。一般认为定义于函数外的对象,以及局部静态变量是有固定地址的。
constexpr
把它所定义的对象置为了顶层const
。
2.5 处理类型
类型别名有两种定义方式,typedef
和别名声明using
:
typedef double wages, *p;
using SI = Sales_item;
如果某个类型别名指代的是复合类型或者常量,那么把它用到声明语句里会产生令人疑惑的结果,比如:
typedef char *pstring
const pstring cstr = 0;
其等效结果为:
char* const cstr;
而不是:
const char* cstr = 0;
因为声明中用到的pstring
是char*
类型的,而如果只是简单改写为后者,则const char
成了其基本类型,这是不对的。
auto
定义的变量必须有初始值。auto
也能在一条语句中声明多个变量,但只有一个基本类型。
auto i = 0, *p = &i;
错误的例子:
int i = 0;
const int ci = i;
auto &n = i, *p = &ci;
当引用作为初值时,真正参与初始化的其实是引用对象的值,所以编译器以引用对象的值作为auto
的类型。
auto
一般会忽略掉顶层const
,而保留底层const
,如果希望推断出auto
类型为顶层const
,则需要明确指出:
const auto f = ci;
decltype
定义了另一种机制,即不计算表达式的值,只推测类型。
decltype(f()) sum = x;
编译器不实际调用函数f
,而是使用当调用发生时f
的返回值类型作为sum
的类型。
decltype
并不像auto
那样对引用和顶层const
做处理,所以可以看到,引用从来都是作为其所指对象的同义词出现,而在decltype
处是一个例外。
int i = 42, *p = &i, &r = i;
decltype(r + 0) b; // r是int&,r + 0是int类型
decltype(*p) c; // 错误,c是int&,必须初始化
解引用指针可以得到指针所指的对象,而且还能给这个对象赋值。
Notes: decltype((variable))
永远是引用。
2.6 自定义数据结构
类体右侧表示结束的花括号必须写一个分号,因为类体后面可以紧跟变量名来表示对该类型的定义。
Notes: 最好不要把类定义和对象的定义放在一起。
类内初始值将用于初始化数据成员,没有初始值的成员将被默认初始化。类内初始值不能使用圆括号。
struct Sales_data {
std::string bookNo;
unsigned units_sold = 0;
double revenue = 0.0;
};
在各个指定的源文件中智能有一处某类的定义,类通常被定义在头文件中,而且类所在的头文件的名字与类的名字相同。头文件还包含const
, constexpr
等实体。
头文件一旦改变,相关的源文件必须重新编译。
预处理变量无视作用域限制。
头文件保护符必须唯一,一般用类名的大写。
第三章:
重要知识点:
3.1 using声明
位于头文件中的代码一般来说不应该有using
声明。
3.2 标准库类型string
初始化string
对象的方式
string s1
string s2(s1)
string s2 = s1 // 等价于上行
string s3("value") // 不包括\0
stirng s4(n, 'C')
string s8 = string(10, 'C') // 差
用cin
读string
时,自动忽略开头的空白,并遇到空白截止。
使用getline()
读取整行,并保留输入时的空白符,读入了末尾换行符,但并不保存在字符串中。如果一开始就是换行符,则所得空串。
可以用empty()
函数来判断空字串。
size()
函数返回值是string::size_type
类型,这是一个无符号类型,且与机器无关,注意加作用域符,不要和其他有符号类型一起运算,以免出错,(混合运算时有符号会被转换为无符号)。可以用auto
来代替输入这个长类型名。
当string
对象和字符字面值相加时,要保证加号两边至少有一个是string
对象,因为字面值不能相加。出于兼容C的考虑,所以C++中字面值不是string
。
C++标准库兼容了C语言的标准库,name.h
和cname
说的是一回事,但后者从属于命名空间std
,前者不然。所以应该使用后者。对于string
中的字符,cctype
中定义了库函数来处理。常用的有:
isalpha(c) // 是否字母
isdigit(c) // 是否数字
isalnum(c) // 是否字母或数字
islower(c) // 是否小写
isupper(c) // 是否大写
tolower(c) // 转为小写
toupper(c) // 转为大写
isspace(c) // 是否空白
ispunct(c) // 是否标点符号
在range for
语句中循环变量使用引用,便可以更改string
对象中的值。
下标访问时,索引的类型为size()
的类型。
3.3 标准库类型Vector
vector
是类模板而非类型,编译器根据模板来创建类或者函数的过程叫做实例化。vector
可以容纳绝大多数的对象作为其元素,但引用不行,因为其不是对象。
初始化方式:
vector<T> v1
vector<T> v2(v1)
vector<T> v2 = v1
vector<T> v3(n, val)
vector<T> v4(n)
vector<T> v5{a, b, c...}
vector<T> v6 = {a, b, c...}
v4
包括了n
个执行了初始化的对象,如果是int
,则初始化为0
。
圆括号用来构造对象,花括号用来列表初始化,先尝试的是列表初始化。
注意以下情况,当初始化时使用了花括号,但提供的值不能用于列表初始化时,便尝试采用构造方式初始化。
vector<string> v7{10};
vector<string> v8{10, "hi"};
在定义vector
对象的时候设定其大小没有必要,如果这么做反而会影响性能,高效的做法是定义一个空vector
,然后运行时向其中添加具体值。
range for
语句体内不应该改变其遍历序列的大小。
size()函数返回值的类型是vector<int>::size_type
。
不要使用下标的形式去访问一个不存在的元素,这种错误不会被编译器发现,但会在运行时产生一个不可预知的值,即缓冲区溢出。
3.4 迭代器介绍
string
对象不属于容器类型,但支持迭代器。
begin
指向容器头元素,end
指向尾元素的下一位置,也叫尾后迭代器。如果容器为空,则二者返回同一个迭代器。迭代器不仅仅指这两个,可以自己定义,然后用begin
赋值之后递增遍历容器。
C++程序员通常在for
循环中使用!=
而不是<
,同更愿意使用迭代器而非下标一样,因为这在所有的容器上都是支持的。
迭代器的类型为iterator
和const_iterator
,如果对象是常量,则使用后者,和size_type
一样,要加上作用域符。
如果使用cbegin()
,cend()
的话,无论对象是否是常量,都会返回const_iterator
。
it->mem;
(*it).mem;
上面两行执行效果相同,注意括号不能省。
任何一个可能改变vector
对象容量的操作都会使迭代器失效。
迭代器相减结果类型为difference_type
,是有符号类型。
3.5 数组
数组的维数应当是一个常量表达式,不能是变量。
不允许用auto
关键字推断类型,数组的类型必须明确指定。数组的元素应为对象,所以不存在引用的数组。
字符数组可以用字符串字面值来初始化。
不允许对数组进行拷贝和赋值。
int (*Parray)[10] = &arr; // 指向一个含有10个整数的数组
int (&arrRef)[10] = arr; // 引用一个含有10个整数的数组
int *(&array)[10] = ptrs; // 数组的引用,该数组有十个指针
采用由内向外法阅读数组类型。
数组下标的类型为size_t
, 定义于cstddef
头文件中,与机器相关,无符号。
使用数组时编译器会把它转换为指针。
string nums[] = {"one", "two", "three"};
string *p2 = nums; // 等价于p2 = &nums[0]
可以通过如下方式获取数组的尾后指针:
int arr[10] = {};
int *e = &arr[10];
在iterator
头文件中定义的函数begin()
和end()
需要将数组名作为变量传入。
两个指针相减结果的类型是ptrdiff_t
,定义在cstddef
头文件中。
C风格的字符串:
这不是一种类型,只是一种约定俗成的写法,定义在cstring
头文件中,常用的方法如下所示:
strlen(p)
strcmp(p1, p2)
strcat(p1, p2)
strcpy(p1, p2)
传入的参数必须是以\0
结尾的数组,否则会访问到未期望的位置。
可以使用以空字符结束的字符数组作为string
运算的一个运算对象,也可以给string
赋值,string
调用c_str()
可以返回一个C风格的字符串。但后续操作如果改变了string
的值,之前返回的的字符串将失效。最好是将数组拷贝一份。
不允许使用vector
对象初始化数组,反之可以。
3.6 多维数组
使用range for语句处理多维数组时, 除了最内层的循环外,其他循环控制变量都应该是引用类型,避免被转换成指针。
第一至三章课后练习解答
"实验楼"平台课程项目作业
本文原载于实验楼