C++ Primer Plus 学习要点(1)
一.C语言中%p与%x的区别
“%p”中的p是pointer(指针)的缩写。%p是打印地址的, 而%x是以十六进制形式打印。
%p是打印地址(指针地址)的,是十六进制的形式,但是会全部打完,即有多少位打印多少位。
32位编译器的指针变量为4个字节(32位),64位编译器的指针变量为8个字节(64位)。
所以,在32位编译器下,使用%p打印指针变量,则会显示32位的地址(16进制的);在64位编译器下,使用%p打印指针变量,则会显示64位的地址(16进制的),左边空缺的会补0。
%x:无符号十六进制整数(字母小写,不像上面指针地址那样补零)
%X:无符号十六进制整数(字母大写,不像上面指针那样补零)
%x、%X和%p的相同点都是16进制,不同点是%p按编译器位数长短(32位/64位)输出地址,不够的补零
二.C++中如何打印字符的地址
1 |
|
解析:
1 | printf("%p\n",&c1); //打印c1字符的地址 |
重点
1 | C++标准库中I/O类对输出操作符<<重载,在遇到字符型指针时会将其当做字符串名来处理,输出指针所指的字符串。既然这样,我们就别让他知道那是字符型指针,所以得进行类型转换,即:希望任何字符型的指针变量输出为地址的话,都要作一个转换,即强制char *转换成void *,如下所示: |
三.简单文件的输入输出
《C++ Primer Plus》第六章第八小节
1. 文件的输出
将文件内容输出至文本中
类比:
1 | // cout控制台输出---cout输出在屏幕上 |
将文件与ofstream关联起来
1 | ofstream outFile; |
示例程序
1 |
|
1 | cout.setf()的作用是通过设置格式标志来控制输出形式,其中ios_base::fixed表示:用正常的记数方法显示浮点数(与科学计数法相对应);ios_base::floatfield表示小数点后保留6位小数。 |
2. 文件的读取
类比
1 | // cout 的文件输入 |
将文件与ofstream关联起来
1 | ifstream inFile; |
使用
1 | double wt; |
is_open()
1 | // 检查文件是否成功被打开使用方法is_open() |
other
1 | eof()方法用于判断最后一次读取数据时候是否遇到EOF,若是返回true |
实例程序
1 |
|
四.指针数组与数组指针
[]的优先级高于*
1 | int (*arr)[4]; // 数组指针,本质为一个指针,指向一个数组,数组中有四个元素,每一个元素都是int 类型----指向二维数组 |
实例:
1 |
|
易混淆
1 |
|
五.数组替代
1.vector模板类
1 | // 头文件 #include <vector> |
2.array模板类(C++11)
1 | // 头文件 #include <array> |
3.超界问题解决
vector 与 array 不会对于错误进行检查
1 | // 例如 |
使用成员函数 at() 进行非法索引的捕获
1 | arr.at(-2); |
六. 函数指针
1 | // 函数定义 |
函数指针
1 | double (*pf)(int) |
区分指针函数
1 | double *pf(int) |
1. 使用指针来调用函数
1 | double pam(int); |
2. 深入(C++ primer plus 202页)
1 |
|
执行结果:
1 | PART1----------- |
七.左值引用 右值引用
1 | 对一个值取地址可以成功取出的为 左值,无法成功取出则为 右值 |
(1). 左值引用
1 | int a = 10; |
(2).右值引用
1 | int a= 10; // 常值是右值,其地址是随机的不确定的 |
八.函数显示具体化与实例化区别
1.形式区别
1 | template<> void Swap<typeName>(typeName &a,typeName &b); |
上述,代码可知,显示具体化声明在关键字 template后包含<>,而显示实例化没有
2.意义区别
显示具体化:
- 显示具体化,指定模板函数中类型,意思是不要使用函数通用的模板来生成函数定义,而是要使用指定的数据类型来生成函数定义
- 显示具体化,实际仍然是隐式实例化,仅在函数调用时,根据指定的参数类型生成指定的函数定义
- 显示具体化为函数模板的特例
显示实例化:
- 显示实例化,直接命令编译器创建特定的实例 —> 无论是否调用,均会生成函数定义,即函数定义一直存在。调用函数时,函数定义直接使用,不调用时函数定义已经存在
- 其具体用途: 先创建模板的某个具体实例,而非使用时在隐式的创建(隐式实例化)。
- 显示实例化,是为了编写库文件提供的。没有实例化的模板无法放置在目标文件(源文件编译之后生成目标文件,目标文件再经过链接生成可执行文件)中。当其他文件代码的目标文件调用(或者链接)该函数时,前提是该函数已经生成了目标文件(即该函数已经生成函数定义实例化后,倘若仍然为函数模板则无法调用)
九、ostream控制格式输出
注:该部分转载至:链接
1.使用流操作算子
C++ 中常用的输出流操纵算子如表 1 所示,它们都是在头文件 iomanip 中定义的;要使用这些流操纵算子,必须包含该头文件
注意:“流操纵算子”一栏中的星号*
不是算子的一部分,星号表示在没有使用任何算子的情况下,就等效于使用了该算子。例如,在默认情况下,整数是用十进制形式输出的,等效于使用了 dec 算子。
流操纵算子 | 作 用 | |
---|---|---|
*dec | 以十进制形式输出整数 | 常用 |
hex | 以十六进制形式输出整数 | |
oct | 以八进制形式输出整数 | |
fixed | 以普通小数形式输出浮点数 | |
scientific | 以科学计数法形式输出浮点数 | |
left | 左对齐,即在宽度不足时将填充字符添加到右边 | |
*right | 右对齐,即在宽度不足时将填充字符添加到左边 | |
setbase(b) | 设置输出整数时的进制,b=8、10 或 16 | |
setw(w) | 指定输出宽度为 w 个字符,或输人字符串时读入 w 个字符 | |
setfill(c) | 在指定输出宽度的情况下,输出的宽度不足时用字符 c 填充(默认情况是用空格填充) | |
setprecision(n) | 设置输出浮点数的精度为 n。 在使用非 fixed 且非 scientific 方式输出的情况下,n 即为有效数字最多的位数,如果有效数字位数超过 n,则小数部分四舍五人,或自动变为科学计 数法输出并保留一共 n 位有效数字。 在使用 fixed 方式和 scientific 方式输出的情况下,n 是小数点后面应保留的位数。 | |
setiosflags(flag) | 将某个输出格式标志置为 1 | |
resetiosflags(flag) | 将某个输出格式标志置为 0 | |
boolapha | 把 true 和 false 输出为字符串 | 不常用 |
*noboolalpha | 把 true 和 false 输出为 0、1 | |
showbase | 输出表示数值的进制的前缀 | |
*noshowbase | 不输出表示数值的进制.的前缀 | |
showpoint | 总是输出小数点 | |
*noshowpoint | 只有当小数部分存在时才显示小数点 | |
showpos | 在非负数值中显示 + | |
*noshowpos | 在非负数值中不显示 + | |
*skipws | 输入时跳过空白字符 | |
noskipws | 输入时不跳过空白字符 | |
uppercase | 十六进制数中使用 A~E。若输出前缀,则前缀输出 0X,科学计数法中输出 E | |
*nouppercase | 十六进制数中使用 a~e。若输出前缀,则前缀输出 0x,科学计数法中输出 e。 | |
internal | 数值的符号(正负号)在指定宽度内左对齐,数值右对 齐,中间由填充字符填充。 |
使用这些算子的方法是将算子用 << 和 cout 连用。例如:
1 | cout << hex << 12 << "," << 24; // 这条语句的作用是指定以十六进制形式输出后面两个数 |
2.setioflags()算子
setiosflags() 算子实际上是一个库函数,它以一些标志作为参数,这些标志可以是在 iostream 头文件中定义的以下几种取值,它们的含义和同名算子一样。
标 志 | 作 用 |
---|---|
ios::left | 输出数据在本域宽范围内向左对齐 |
ios::right | 输出数据在本域宽范围内向右对齐 |
ios::internal | 数值的符号位在域宽内左对齐,数值右对齐,中间由填充字符填充 |
ios::dec | 设置整数的基数为 10 |
ios::oct | 设置整数的基数为 8 |
ios::hex | 设置整数的基数为 16 |
ios::showbase | 强制输出整数的基数(八进制数以 0 开头,十六进制数以 0x 打头) |
ios::showpoint | 强制输出浮点数的小点和尾数 0 |
ios::uppercase | 在以科学记数法格式 E 和以十六进制输出字母时以大写表示 |
ios::showpos | 对正数显示“+”号 |
ios::scientific | 浮点数以科学记数法格式输出 |
ios::fixed | 浮点数以定点格式(小数形式)输出 |
ios::unitbuf | 每次输出之后刷新所有的流 |
ios::stdio | 每次输出之后清除 stdout, stderr |
多个标志可以用|
运算符连接,表示同时设置。例如:
1 | cout << setiosflags(ios::scientific|ios::showpos) << 12.34; |
如果两个相互矛盾的标志同时被设置,如先设置 setiosflags(ios::fixed),然后又设置 setiosflags(ios::scientific),那么结果可能就是两个标志都不起作用。因此,在设置了某标志,又要设置其他与之矛盾的标志时,就应该用 resetiosflags 清除原先的标志。例如下面三条语句:
1 | cout << setiosflags(ios::fixed) << 12.34 << endl; |
3.调用cout成员函数
ostream 类有一些成员函数,通过 cout 调用它们也能用于控制输出的格式,其作用和流操纵算子相同.
成员函数 | 作用相同的流操纵算子 | 说明 |
---|---|---|
precision(n) | setprecision(n) | 设置输出浮点数的精度为 n。 |
width(w) | setw(w) | 指定输出宽度为 w 个字符。 |
fill(c) | setfill (c) | 在指定输出宽度的情况下,输出的宽度不足时用字符 c 填充(默认情况是用空格填充)。 |
setf(flag) | setiosflags(flag) | 将某个输出格式标志置为 1。 |
unsetf(flag) | resetiosflags(flag) | 将某个输出格式标志置为 0。 |
setf 和 unsetf 函数用到的flag
,与 setiosflags 和 resetiosflags 用到的完全相同。
1 | cout.setf(ios::scientific); |
十、类的自动类型转换与强制转换
1.构造函数用作自动类型转换的转换函数
只接受一个参数的构造函数才可以作为转换函数
1 | Stock::Stock(double lab); // 构造函数 |
explicit
可通过构造函数前,显示添加explicit,关闭该自动特性
1 | explicit Stock::Stock(double lab); |
2.转换函数
转换函数,定义了从 某种类型 到 类类型的转换
1 | Stock::Stock(double lab); // 构造函数 |
创建转换函数
1 | operator typeName(); |
explicit
若类方法中,只定义了 类类型到一种类型 的转换函数,这将避免二义性,而使得可以使用隐式转换
1 | //类声明中 |
explicit关键词,可以防止这样的隐式转换,只允许显示转换
1 | explicit operator double() ;// 定义转换函数 |
十一、类的静态成员变量
静态类成员:无论创建多少个类对象,程序只创建一个静态类变量的副本
1 | // 定义在StringBad类声明中 |
不能在类声明中初始化静态成员变量,声明描述了如何分配内存,但是并不分配内存,若在类声明(.h头文件中)初始化静态成员变量,当将该头文件引入别的文件时,会违背单定义的原则
可以在类实现中(.cpp文件中)初始化静态变量
1 | // 初始化在类实现文件中 |
若静态成员变量是const类型,则可以在类声明中初始化
1 | // 类声明中进行初始化 |
十二、复制构造函数
复制构造函数用于将一个对象复制到新创建的对象中。即用于初始化过程中(包括按值传递参数),而非常规的赋值过程,类的复制构造函数原型通常如下:
1 | Class_Name(const Class_Name &) |
1.何时调用赋值构造函数
1 | 1.当函数按值传递类对象时(意味着创建原始变量的副本,编译器将生成临时对象,将使用复制构造函数) |
按值传递对象将调用复制构造函数,因此尽可能按引用传递对象,节省调用复制构造函数的时间,节省创建副本的时间
2.定义显示复制构造函数
定义一个显示的复制构造函数进行深度复制,复制构造函数,应当复制对象的数据内容并将对象副本的地址赋给令一个对象,而不仅仅是复制对象的地址,从而去引用该对象。
仅仅复制对象的地址赋给其他对象,从而引用该对象,为浅复制,当调用析构函数进行释放内存时,则会将一个对象进行两次释放。
一般的C++标准库均有复制构造函数的重载,若一类中的成员无使用动态的内存分配,程序调用默认的复制构造函数无影响,因为程序结束时,调用析构函数无动态的内存释放,不会对同一个开辟的内存空间进行两次释放。
重点
1 | 如果类中包含了使用New初始化的指针成员,应当定义一个复制构造函数,以复制指向的数据,而非指针(对象地址),这被称为深度复制。 |
3.赋值问题
将一个对象赋值给另外一个对象,也会出现与隐式复制构造函数相同的问题,因此需要提供复制赋值运算符的定义进行(深度复制)
书中:P356
十三、成员列表的初始化
对类中声明的const常量无法在类定义中对其进行赋值,因为常量只能在声明时进行初始化,因此可以使用成员初始化列表的方式,对常量进行初始化操作。成员列表初始化会在执行构造函数之前,对创建的对象进行初始化。成员列表初始化的方式只能用于构造函数。
注意:
1 | 1.只能用于构造函数 |
1 | Queue::Queue(int qs):qsize(qs) |
qsiz
e 是在类声明中定义的一个常量,无法通过 **qsize =qs**
对常量进行赋值操作,只能初始化,原因是从概念上来说,调用构造函数时,对象将在括号中的代码执行之前被创建,因此调用 Queue(int qs) 构造函数将导致程序首先给四个成员变量分配内存(此时分配内存相当于在做初始化),然后程序流程进入到括号中,使用常规的赋值方式,将值存储到内存中,因此对于 const 数据成员,必须在执行构造函数体之前,即创建对象时进行初始化
成员初始化列表,不用写在类声明中,只需要写在类定义中
十四、友元
- 友元函数
- 友元类
- 友元成员函数
友元函数是一种非成员函数,可以赋予该函数与类的成员函数相同的访问权限,可以访问类中的私有成员变量
友元函数,将普通的函数,通过friend
修饰,但是该修饰只出现在函数原型上,即类声明中。不要在类定义中使用关键字friend
十五、派生类的构造函数
1.成员初始化列表调用基类构造函数
派生类的构造函数必须使用基类的构造函数
创建派生类对象时,程序首先创建基类对象。从概念上说,这意味着基类对象应当在程序进入派生类构造函数之前被创建。C++使用成员初始化列表的方式创建
下面是一个派生类RatedPlayer
继承自基类TableTennisPlayer
构造函数的代码实例:
1 | // 只在类实现中这样写,类声明中不写成员列表初始化 |
上述实例代码,:TableTennisPlayer(fn,ln,ht)
是成员初始化列表。它是可执行代码,调用TableTennisPlayer构造函数。通过成员初始化列表,使用基类的构造函数为基类的私有成员进行赋值。
先创建基类对象(通过成员初始化列表的方式,隐式创建),在通过派生类构造函数创建派生类对象
2.省略成员初始化列表
若省略成员初始化列表,实例代码如下:
1 | RatedPlayer::RatedPlayer(unsigned int r,const string & fn,const string & ln,bool ht) |
必须首先创建基类对象,若不调用基类构造函数(即使不使用成员初始化列表方式调用基类的构造函数),程序也将使用默认基类构造函数,以下代码与上述等价:
1 | // 等价于调用默认的构造函数 |
除非使用默认的构造函数,否则应该显示的调用正确的基类构造函数
1 | RatedPlayer::RatedPlayer(unsigned int r,const TableTennisPlayer & tp):TableTennisPlayer(tp) |
tp
的类型是 TableTennisPlayer &
,因此将调用基类的复制构造函数,基类若没有定义复制构造函数,编译器将自动生成一个。
若愿意,也可以使用成员初始化列表对派生类成员进行初始化,代码实例如下:
1 | RatedPlayer::RatedPlayer(unsigned int r,const TableTennisPlayer & tp):TableTennisPlayer(tp),rating(r) |
3.要点
1 | 首先创建基类对象 |
注意:创建派生类对象时,程序首先调用基类构造函数,再调用派生类构造函数,基类构造函数负责初始化继承的数据成员;派生类构造函数主要用于初始化新增的数据成员。派生类构造函数总是调用几个基类的构造函数。可以使用初始化列表的语法指明要使用的基类构造函数,否则将使用默认的基类构造函数
派生类对象过期(应该被释放时),程序首先调用派生类的析构函数,然后再调用基类的析构函数
十六、派生类与基类之间的关系
派生类可以使用基类的方法,前提是方法非私有
基类的指针与引用均可以不在显示转换的情况下指向派生类对象,但是基类指针与方法均只能调用基类的方法。**(单向)**,派生类新增的方法与成员基类的指针与引用无法调用
// ball 是 basketball 的基类 basketball object("vs"); ball *f_object1 = &object; // 基类指针指向派生类对象 ball &f_object2 = object; // 基类引用指向派生类对象
1
2
3
4
5
6
7
8
9
10
* 可以将派生类对象赋给基类对象**(会使用隐式的重载运算符)**
* ```c
// ball 是 basketball 的基类
basketball object("vs");
ball f_object1 = object; // 派生类对象赋值给基类对象
// 这种情况,会使用隐式的重载运算符
ball &f_object1 = (const ball &) object; // 基类引用指向派生类对象,object的基类部分,将被复制给f_object1
在派生类中若定义了与基类相同名称的方法(重写),则需要通过
::
作用域解析运算符来调用基类的方法// ball 是 basketball 的基类 // 若基类与派生类中均定义了 view()方法,在派生类中想使用基类的方法,则需要使用 基类名称::方法名称 调用基类方法 ball::view()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
## 十七、虚方法
在类的类声明的函数原型中通过`virtual`关键字可以将函数进行**虚函数**的声明。**`virtual`关键词只用于类声明的方法原型中**,同友元函数的关键词`friend`
**在基类中将派生类会重新定义的方法,声明为虚方法,方法在基类中被声明为虚方法之后,在派生类中不用指明关键字 virtual 也会自动成为虚方法**,好习惯,还是将**派生类中的虚方法通过`virtual`声明出来,增加代码的可读性**
**基类声明一个虚的析构函数,这样可以确保释放对象时,按照正确的顺序调用析构函数**
### 1.重点
**利用基类的指针与引用指向派生类对象,若方法没有声明为`virtual` 虚方法,程序将根据指针或者引用的类型去选择方法;若使用了`virtual`声明为了虚方法,程序则会根据指针或者引用则会根据指向对象的类型来选择方法**
* 若`view()`没有被声明为虚方法,`view()`方法在基类与派生类中均有定义
* ```c
// ball 是 basketball 的基类
basketball object("vs");
ball opp("str");
ball &f_object1 = opp; // 基类引用指向基类对象
ball &f_object2 = object; // 基类引用指向派生类对象
ball.view(); // 引用类型为基类引用,因此均是调用基类中定义的 view() 方法
ball.view(); // 引用类型为基类引用,因此均是调用基类中定义的 view() 方法
若
view()
被声明为虚方法,view()
方法在基类与派生类中均有定义// ball 是 basketball 的基类 basketball object("vs"); ball opp("str"); ball &f_object1 = opp; // 基类引用指向基类对象 ball &f_object2 = object; // 基类引用指向派生类对象 ball.view(); // 引用类型为基类引用,引用指向的对象为基类对象,因此使用基类的方法 ball.view(); // 引用类型为基类引用,引用指向的对象为派生类对象,因此使用派生类的方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
### 2.虚函数注意事项
* 若定义的类被用作基类,则应该将那些**派生类中重新定义的方法声明为虚函数**
* **构造函数**不能为虚函数
* **析构函数应当是虚函数,除非类不做基类**,即使类不做基类,将其析构函数定义为虚函数也没有问题
* **友元函数不能为虚函数,因为友元函数不是成员函数,为非成员函数**
## 十八、为何需要虚的析构函数
**若析构函数不是虚函数**,通过一个基类指针/引用指向派生类对象时,当基类指针/引用被释放时,则将只调用对应于指针或者引用类型的析构函数。,这意味着只有基类的对象被调用,即使指向了一个派生类对象。
**若析构函数为虚函数**,通过一个基类指针/引用指向派生类对象时,当基类指针/引用被释放时,则将先调用派生类即**指针/引用指向对象的析构函数**,然后**自动调用基类的析构函数**即指针/引用类型的析构函数。
**总结:**使用虚的析构函数,可以确保正确的析构函数序列被调用。**好习惯:在基类中声明虚的析构函数**
## 十九、抽象基类(纯虚函数)
抽象基类**(ABC)**包含其所有派生类的共有(共性)部分
C++通过纯虚函数来提供为实现的函数,**纯虚函数在结尾处为=0**
```c
virtual double Area()=0; // 纯虚函数
若一个类需要成为抽象基类(ABC)则该基类中必须包含一个纯虚函数,含有纯虚函数的类(ABC)只能做基类,无法创建该类对象。但是即使该基类为抽象的,我们仍可以在实现文件中提供方法的定义
二十、类模板
1.创建类模板与模板函数分为以下三步:
- 模板类—如下代码打头:
template<class Type>
- 每个函数头都将以相同的模板声明打头:
template<class Type>
- 最后需要将类限定符 从
类名::
改为类名<Type>::
重点:
这些模板不是类和成员函数的定义,他们是c++编译器指令,说明了如何生成类和成员函数定义,模板的具体实现被称为实例化或者具体化。不能将模板成员函数放在独立的实现文件中,由于模板不是函数,他们不能单独编译. 模板必须与特定的模板实例一起使用,最简单的方法是:将所有模板信息放在一个头文件中
例:
1 | template <class Type> |
2.模板隐式实例化、显示实例化、具体化
① 隐式实例化:给模板传入特定的类型参数并创建类对象时,会同时生成类声明,该操作在程序运行阶段生成类定义
② 显示实例化:使用template打头,并给模板传入特定的类型参数(无需创建类对象)。该操作在程序编译阶段产生类声明
③ 具体化:给模板使用具体的类型参数生成类声明。显/隐实例化均通过具体的类型生成类声明。实例化属于具体化
类模板与函数模板很相似,均可以有隐式实例化、显示实例化和显示具体化,都统称为具体化,模板以泛型的方式描述类,而具体化使用具体的类型生成类声明
模板仅仅描述类的样子并无类的定义与声明,而通过具体化可生成类声明
二十一、异常
1.调用abort()函数
1 |
|
abort()函数会在出现a==-b
的情况时会直接中止程序,程序运行阶段错误
运行结果:
1 | /home/zxz/c++/demo/9/cmake-build-debug/9 |