C++ Primer(正在更新)
写在前面
有些我认为过于基础,我已经会的东西不会进行记录。
第 Ⅰ 部分 C++基础
第二章 变量和基本类型
const限定符
const修饰的变量初始化,可以利用一个非const对象来赋值,它会将值拷贝过来,就和原来的对象无关了。
const对象仅在文件内有效,多个文件有同名const对象,其实都是独立的变量,如果想要多文件同名const对象是一个对象,应都加上 extern
。
初始化和对const的引用
1
2
double dval = 3.14;
const int &ri = dval;
此处对ri的操作应该是 int
,但他是 double
,所以编译器会将上述代码变成:
1
2
const int temp = dval;
const int &ri = temp;
接下来讨论当ri不是常量时,此时ri绑定的是临时量,而不是dval,这是一种非法行为。
顶层const
顶层const表示对象本身不可修改,底层const表示对象指向的对象不可修改。
- 顶层 const 主要用于限定变量的不可修改性,它常用于定义常量,如常量整数或常量指针。
- 底层 const 主要用于保护通过指针或引用访问的对象,使得不能通过这些指针或引用修改对象的值。这在函数形参或接口设计中非常重要,确保数据不会被意外修改。
1
2
3
4
5
6
7
const int v2 = 0; // v2 是顶层 const,表示 v2 的值不可修改。
int v1 = v2; // v1 是普通的 int 类型,可以修改。
int *p1 = &v1, &r1 = v1; // p1 是指向 v1 的指针,v1 的值可以通过 p1 修改;
//r1 是 v1 的引用,同样可以修改 v1 的值。
const int *p2 = &v2, *const p3 = &i, &r2 = v2; // p2 是底层 const,表示不能通过 p2 修改 v2 的值;
//p3 是顶层和底层 const,既不能修改 p3 指向的对象,也不能改变 p3 本身的值;
//r2 是底层 const,不能通过 r2 修改 v2 的值。
1
2
3
4
5
r1 = v2; // 合法,r1 是 v1 的引用,r1 的值可以被赋值。
p1 = p2; // 不合法,p1 是 `int*` 类型,p2 是 `const int*`,不能将指向 `const` 对象的指针赋给普通指针。
p2 = p1; // 合法,p2 是 `const int*`,允许将普通指针赋值给 `const int*`,因为 p2 保证不修改值。
p1 = p3; // 不合法,p1 是 `int*` 类型,p3 是 `const int*`,不能将指向 `const` 对象的指针赋给普通指针。
p2 = p3; // 合法,p2 和 p3 都是 `const int*` 类型,可以相互赋值。
第五章 语句
TRY语句和异常处理
当程序的某部分检测到一个它无法处理的问题时,就需要用到异常处理。
在C++语言中,异常处理包括:
- throw表达式
- try语句块,以关键字
try
开始,并以一个或多个catch
子句结束。try
语句块中代码抛出的异常通常会被某个catch子句处理。 - 异常类 用于在
throw
表达式和相关的catch
子句间传递异常的具体信息。
throw表达式
抛出异常将终止当前的函数,类比 return
。
try语句块
程序进入某个 try
语句块,处理异常之后程序会跳到 try
语句块最后一个 catch
子句之后的语句。
函数在寻找处理代码的过程中退出
例如,一个
try
语句块可能调用了包含另一个try
语句块的函数,新的try
语句块可能调用了包含又一个try
语句块的新函数,以此类推。寻找处理代码的过程与函数调用链刚好相反。当异常被抛出时,首先搜索抛出该异常的函数。如果没找到匹配的
catch
子句,终止该函数,并在调用该函数的函数中继续寻 找。如果还是没有找到匹配的catch
子句,这个新的函数也被终止,继续搜索调用它的 函数。以此类推,沿着程序的执行路径逐层回退,直到找到适当类型的catch
子句为止。类比回溯如果最终还是没能找到任何匹配的
catch
子句,程序转到名为terminate
的标准库函数。一般执行该函数将导致程序非正常退出。
编写异常安全的代码非常困难。
在异常发生期间正确执行了“清理”工作的代码被称为异常安全的代码,非常难编写。
标准异常
异常类型只有一个名为 what
的成员函数,没有任何参数,返回值是一个 const char*
函数
函数基础
函数声明
我们建议变量在头文件中声明,在源文件中定义。与之类似,函数也应该在头文件中声明而在源文件中定义。
我们把函数声明在头文件中,就能确保同一函数的所有声明保持一致。而且一旦我们想改变函数的接口,只需要改变一条声明即可。
1
2
3
4
5
6
7
8
// geometry.h
#ifndef GEOMETRY_H
#define GEOMETRY_H
// 函数声明
double calculateArea(double width, double height);
#endif
1
2
3
4
5
6
7
// geometry.cpp
#include "geometry.h"
// 函数实现
double calculateArea(double width, double height) {
return width * height;
}
1
2
3
4
5
6
7
8
9
// main.cpp
#include "geometry.h"
#include <iostream>
int main() {
double width = 5.0, height = 3.0;
std::cout << "Area: " << calculateArea(width, height) << std::endl;
return 0;
}
想改变 calculateArea
的返回类型就只要改变 geometry.h
中的声明。
参数传递
main:处理命令行选项
假设 main 函数位于可执行文件 prog 之内,我们可以向程序传递下面的选项:
1
prog -d -o ofile data0
1
int main(int argc, char *argv[]) {}
以上面提供的命令为例:
1
2
3
4
5
argv[0] = "prog"//程序名字,也可以指向空字符串
argv[1] = "-d"
argv[2] = "-o"
·······
含有可变形参的函数
initializer_list
与 vector
异同点。
- 同:都是模板类型,定义时必须说明所含元素类型。
- 异:
initializer_list
中的元素永远是常量值。
特殊用途语言特性
constexpr
定义 constexpr
函数的方法要遵循几项约定:
- 函数的返回类型及所有的形参的类型都得是字面值类型
- 函数体中必须有且只有一条
return
语句:1 2
constexpr int new_sz() { return 42; } constexpr int foo = new_sz();
编译器把 constepxr
的调用替换成结果值。constepxr
函数被隐式地指定为内联函数。
需要注意的是 constepxr
函数不一定返回常量表达式。
调试帮助
NDEBUG 预处理变量
assert 的行为依赖于 NDEBUG ,如果定义了 NDEBUG ,assert 就会什么都不做。
可以用于编写自己的条件调试代码:通过 ifndef
和 endif
来控制输出。
_ _func_ _
当前调试的函数的名字_ _FILE_ _
当前文件名_ _LINE_ _
当前行号_ _TIME_ _
当前文件的编译时间_ _DATE_ _
当前文件的编译日期
函数指针
bool (*pf)(const string &, const string &)
主要*pf一定要有括号。
当我们使用重载函数时,上下文必须清晰地界定到底应该选择调用哪个函数。如果定义了指向重载函数的指针:
重载函数的指针
1 2 3 4
void ff(int*); void ff(unsigned int*); void (*pf1)(unsigned int*) = ff; // pf1指向ff(unsigned)
编译器通过指针类型决定选用哪个函数,指针类型必须与重载函数中的某一个精确匹配。
1 2
void (*pf2)(int) = ff; // 错误:没有任何一个ff与该形参列表匹配 double (*pf3)(int*) = ff; // 错误:ff和pf3的返回类型不匹配
函数指针的形参
1 2 3
//第三个形参是函数类型,它会自动地转换成指向函数的指针 void useBigger(const string &s1, const string &s2, bool pf(const string &, const string &))
1 2 3
//显示地将形参定义成指向函数的指针 void useBigger(const string &s1, const string &s2, bool (*pf)(const string &, const string &))
类
访问控制与封装
类的作者常常需要定义一些辅助函数,尽管这些函数定义的操作从概念上属于类的接口的组成部分,但它们实际上并不属于类本身。
定义非成员函数的方式与定义普通函数一样。通常,这些函数的声明和定义是分开的,声明会放在一个头文件中,以便用户在其他地方调用时可以找到这个接口。
实例如下:
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
31
32
// Point.h 头文件
#ifndef POINT_H
#define POINT_H
#include <iostream>
// 定义 Point 类
class Point {
public:
Point(int x, int y) : x_(x), y_(y) {}
// 友元声明,以允许非成员函数访问私有成员
friend Point add(const Point& p1, const Point& p2);
friend void print(const Point& p);
private:
int x_, y_; // 点的 x 和 y 坐标
};
// 非成员函数:add
// 定义一个与 Point 类相关的函数,但不属于类本身
Point add(const Point& p1, const Point& p2) {
return Point(p1.x_ + p2.x_, p1.y_ + p2.y_);
}
// 非成员函数:print
// 定义另一个与 Point 类相关的非成员函数,用于打印点的坐标
void print(const Point& p) {
std::cout << "(" << p.x_ << ", " << p.y_ << ")" << std::endl;
}
#endif // POINT_H
构造函数再探
- 某些类不能依赖于合成的默认构造函数 P236
已有构造函数:如果类已经显式定义了构造函数,编译器将不会自动生成默认构造函数。这种情况下,如果需要使用默认构造函数,必须手动定义。
- 成员需要显式初始化:
- 如果类中有需要显式初始化的成员,例如某些内置类型的变量或复杂类型(如数组、指针等),这些成员无法通过默认构造函数自动初始化,需要用户手动编写构造函数来初始化它们。
- 若类的成员包含复杂类型或自定义类类型的成员,默认构造函数无法满足初始化要求,因此应自行定义构造函数。
禁止生成默认构造函数的情境:在一些特殊情况下,例如类中包含指向其他类的引用成员或常量成员,默认构造函数无法自动生成,因为这些成员必须在初始化时提供值。
- 防止错误的默认构造:对于一些特定的类,编译器生成的默认构造函数可能导致错误操作,这种情况下最好自行定义构造函数,确保初始化符合需求。
访问控制与封装
友元
友元声明只能在类定义的内部。友元不是类的成员也不受它所在区域访问控制级别的约束。
通过声明友元,可以让其他类或者其他类的特定成员函数访问自身的所有成员。
令成员函数作为友元:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
class Screen; // 前向声明 Screen 类,以便在 Window_mgr 中引用 class Window_mgr { public: void clear(); // 在 Window_mgr 中声明 clear 函数,但还不能定义 }; class Screen { friend void Window_mgr::clear(); // 将 Window_mgr::clear 声明为 Screen 的友元 //Screen功能 //······ }; // 现在我们可以定义 clear 函数了 void Window_mgr::clear() { // 假设我们要将 Screen 对象的内容清除为 ' ' (空格) Screen screen(5, 5, 'X'); // 创建一个 Screen 对象用于演示 screen.contents = std::string(screen.height * screen.width, ' '); // 直接访问私有成员 screen.display(); // 显示清除后的 Screen }
封装的益处
保护对象状态:封装确保用户代码无法随意破坏对象的内部状态。通过将数据成员设为
private
,类的作者可以在不影响用户代码的情况下,随时修改类的实现细节,保证代码的灵活性和安全性。简化维护:封装限制用户代码对数据的直接访问,从而减少因代码修改而带来的错误。如果类的数据是
public
的,任何对数据的修改都可能影响到依赖该类的用户代码,增加了维护难度和错误修正的复杂性。而通过private
访问权限的设置,能够使修改更加安全,不需要频繁地更改用户代码,简化了代码维护。
类的其他特性
返回*this的成员函数
1
2
3
4
5
6
7
8
9
10
11
Screen &Screen::move(pos r, pos c) {
//······
return *this;
}
Screen &Screen::set(char ch) {
//······
return *this;
}
通过返回 *this 我们就可以实现链式编程:myScreen.move(4, 0).set('#')
。
需要注意的是,如果链式函数中某个函数返回的是常量引用,后续函数就不能有修改操作。
基于const的重载
非常量版本的函数对于常量对象是不可用的,所以我们可以重载一个常量版本函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
class Screen { public: // 根据对象是否是 const 重载了 display 函数 Screen &display(std::ostream &os) { do_display(os); return *this; } const Screen &display(std::ostream &os) const { do_display(os); return *this; } private: // 该函数负责显示 Screen 的内容 void do_display(std::ostream &os) const { os << contents; } // 其他成员与之前的版本一致 };
调用:
1 2 3 4
Screen myScreen(5, 3); const Screen blank(5, 3); myScreen.set('#').display(cout); // 调用非常量版本 blank.display(cout); // 调用常量版本
总结如下:
- 对于公共代码使用私有辅助函数
- 避免重复代码:使用私有的辅助函数
do_display
可以避免在多个地方重复相同的代码,提高代码的可维护性。 - 便于扩展和维护:随着类的复杂度增加,
display
函数可能会变得更加复杂。将重复操作放入私有的do_display
函数中,有助于在一个位置集中实现核心操作,方便日后调整。 便于调试和日志记录:可以在
do_display
中增加测试和调试信息,不必在多个位置进行更改。删除或修改这些信息也会更加方便。- 其他特性
- 定义私有函数
do_display
并不会带来运行时开销,因为编译器会内联这些调用。 - 这种设计模式在C++中很常见,通过将常用的功能封装到私有函数中,便于在类中分组和组织相似的操作。
- 定义私有函数
- 避免重复代码:使用私有的辅助函数
名字查找与类的作用域
寻找类型名的重命名,遵循的规则是:由内而外进行寻找。
所以以下情况 balance
返回的将是 double
类型。
1
2
3
4
5
6
7
8
9
typedef float Money;
class Account {
typedef double Money;
public:
Money balance() { return bal; }
private:
Money bal;
}
❗类内的 typedef
请定义在类开始的时候,如果成员使用了外层作用域的某个名字,类内后来重新定义改名字,会发生错误。
构造函数再探
隐式的类类型转换
在 Sales_data
类中,接受 string 地构造函数和接受 istream 的构造函数分别定义了从这两种类型向 Sales_data 隐式转换的规则:
1
2
3
4
string null_book = "9-99-999";
//自动创建一个临时的Sales_data对象
item.combine(null_book);
需要注意的是编译器只会自动地执行一步类型转换,所以
1
item.combine("9-99-999")
是会报错的,”9-99-999” 并不是string对象。
抑制构造函数定义的隐式转换
通过 explicit 关键字,禁止构造函数隐式创建类本身的对象。
但是我们可以通过显示构造对象来实现。
1
item.combine(Sales_data(null_book));
聚合类
1
2
3
4
5
6
struct Data {
int ival;
string s;
}
Data val1 = { 0, "Anna" };
注意成员都是 public 的。
字面值常量类
- 数据成员都必须是字面值类型
- 类必须至少还有一个constexpr构造函数
- constexpr构造函数必须初始化所有数据成员
类的静态成员
静态数据成员不属于任何一个对象,一般来说,不能在类的内部初始化静态成员,假如提供字面值,则可以在类内初始化静态成员。
存在于程序的整个生命周期中。
第 Ⅱ 部分 C++标准库
第九章 顺序容器
顺序容器操作
不要保存end返回的迭代器
当我们添加/删除容器的元素后,原来end返回的迭代器总是会失效,所以我们绝不要保存尾迭代器的值,用到的时候直接调用。