第十二章 动态内存

全局对象:程序启动时分配,程序结束时销毁
局部自动对象:进入其定义所在程序块时创建,离开块时销毁
局部static对象:第一次使用前分配,在程序结束时销毁
动态对象:生存期由程序控制

12.1 动态内存和智能指针

1 new 分配内存和 delete 释放内存
new 在动态内存中为对象分配空间并返回一个指向该对象的指针
delete 接受一个动态对象的指针,销毁该对象
缺点:忘记 delete 导致内存泄漏;在还需要使用时 delete ,产生非法引用内存的指针

2 使用智能指针来管理动态内存——C++11
行为类似常规指针,但会自动释放所指向的对象
头文件为 memory

  • shared_ptr 允许多个指针指向一个对象
  • unique_ptr 独占所指向的对象
  • weak_ptr 指向 shared_ptr 管理的对象

3 shared_ptr

  • shared_ptr 类是模板类,需要提供指针指向的类型
  • 默认初始化的智能指针保存着空指针
  • 使用方法与普通指针类似

4 make_shared 函数
在动态内存中分配一个对象并初始化它,返回指向此对象的 shared_ptr
传给 make_shared 函数的参数用来初始化对象,类比容器的 emplace 成员
不传递参数,则进行值初始化

1
2
shared_ptr<string> p6 = make_shared<string>();
auto p6 = make_shared<string>();

5 shared_ptr 的拷贝和赋值
shared_ptr 有一个引用计数,每次拷贝 shared_ptr ,计数器都会递增;当给 shared_ptr 赋予一个新值或是 shared_ptr 被销毁,计算器会递减;一旦计数器变为0,自动释放自己管理的对象

6 shared_ptr 自动销毁所管理的对象
shared_ptr 的析构函数会递减它所指向的对象的引用计数,如果计数变为0, shared_ptr 的析构函数会销毁对象,并释放它所占用的内存

7 为什么要用动态内存

  • 程序不知道自己需要多少对象
  • 程序不知道所需对象的准确类型
  • 程序需要在多个对象间共享数据

8 使用动态生存期的资源的类
在类的内部,采用 shared_ptr 来动态管理内存,从一个对象拷贝的对象会共享这部分内存,并且直到最后一个对象被销毁时,这部分内存才被释放

9 直接管理内存的类不能以来类对象拷贝、赋值、销毁操作的任何默认定义

  • 默认操作的拷贝为浅拷贝,也就是说拷贝的对象和原对象的指针指向同一块内存地址;在此情况下,如果定义了析构函数多次 delete 指针,就会造成内存二次释放
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <iostream>
class test1 {
private:
int* p = new int(1); // 动态分配内存
public:
~test1() {
delete p; // 析构时释放内存
}
void show() const {
std::cout << "Value: " << *p << std::endl;
}
};
int main() {
test1 obj1; // 创建对象 obj1
test1 obj2 = obj1; // 默认拷贝构造(浅拷贝,两个对象共享同一块内存)
obj1.show();
obj2.show();
// 当程序结束时,obj1 和 obj2 的析构函数都会释放同一块内存
// 导致程序崩溃或未定义行为
return 0;
}

10 使用 new 动态分配和初始化对象

  • new 返回的是一个指向对象的指针
  • 默认初始化,默认构造函数
  • 直接初始化,可以用 (1){1, 2} 等来初始化
  • 值初始化, (){} 内部不带参数
    • 类类型:值初始化和默认初始化都会调用默认构造函数初始化,没有区别
    • 内置类型:值初始化的内置对象有着良好定义的值,而默认初始化的对象的值是为定义的
  • auto 自动推断类型的直接初始化,只能当括号中仅有单一初始化器才可以用

11 动态分配的 const 对象
new 返回一个指向 const 的指针

12 内存耗尽

  • 内存耗尽后 new 会爬出一个类型为 bad_alloc 的异常
  • 可以通过 int *p1 = new (nothrow) int; 的方法阻止异常抛出,分配内存失败时放回空指针
    • 允许向 new 传递参数

13 释放动态内存
delete 表达式将动态内存归还给系统

  • 销毁给定指针指向的对象
  • 释放对象对应的内存
    传递给 delete 的指针必须指向动态分配的内存,或者是一个空指针

14 动态对象的生存期直到被释放时为止
在一个函数块内分配的动态内存,如果指向这个动态内存的指针没有传出去,此时在没有 delete 的情况下离开代码块,则这块内存永远不会被回收了

15 delete 之后重置指针值

  • delete 只是释放内存,指针还是存在的,所以 delete 之后会造成空悬指针,最好在 delete 之后将指针赋为 nullptr
  • 当有多个指针指向同一块内存地址时, 无法确保将所有的空悬指针赋为空指针

16 shared_ptrnew 结合使用

  • 可以用 new 返回的指针来初始化智能指针
  • 但不能将内置指针隐式转换为一个智能指针,必须使用直接初始化形式来初始化一个智能指针
  • 用来初始化智能指针的普通指针必须指向动态内存,因为智能指针会默认 delete 释放它所关联的对象

17 不要混合使用普通指针和智能指针

  • 将同一块内存绑定到多个独立创建的 shared_ptr 会导致智能指针多次析构释放同一块内存
  • 将普通指针绑定到 shared_ptr 之后,当 shared_ptr 析构了完,普通指针指向的动态内存被释放了

18 不要用 get 初始化另一个智能指针或为智能指针赋值
会出现上条的类似情况,动态内存被新的智能指针释放

19 reset

1
2
3
4
5
shared_ptr<int> p = make_shared<int>();
shared_ptr<int> q = make_shared<int>();
p.reset(); // 若p是唯一指向其对象的shared_ptr reset会释放此对象 若传递了可选的参数内置指针q
p.reset(q); // 会令p指向q 否则会将p置为空 若还传递了参数d 将会调用d而不是delete来释放q
p.reset(q, d);

20 使用智能指针确保内存在异常时释放

21 使用智能指针来保证哑类的资源释放——删除器
在创建指向哑类对象的智能指针时,传递一个 deleter 函数给智能指针,让它能够在析构时调用

22 unique_ptr 独占它所指的对象
不支持普通的拷贝或赋值操作

22 unique_ptr 必须采用直接初始化形式

22 release
放弃对指针的控制权,返回 unique_ptr ,将调用者置为空
必须要有接受这部分动态内存的“接收单位“,否则这部分动态内存泄漏了

23 传递 unique_ptr 参数和返回 unique_ptr
可以拷贝或赋值一个将要被销毁的 unique_ptr

24 向 unique_ptr 传递删除器

  • 必须指明删除器类型
  • 必须提供一个指定类型的可调用对象

25 weak_ptr 指向一个由 shared_ptr 管理的对象,但不改变引用 shared_ptr 的引用计数
weak_ptr 指向的对象可能被销毁,故在使用之前最好用 lock 来判断是否被销毁

第十五章 继承和动态绑定

概述

面对对象程序设计的核心思想:数据抽象、继承和动态绑定

  • 数据抽象:将类的接口和实现分离
  • 继承:定义相似的类型并对其相似关系建模
  • 动态绑定:可以在一定程度上忽略相似类型的区别,以统一的方式使用它们的对象
    继承和动态绑定的优点:
  • 可以更容易定义与其他类相似但不完全相同的新类
  • 使用这些彼此相似的类编写程序时,可以在一定程度上忽略掉它们的区别

继承

在已有的基类上面派生出新的类型——派生类

不涉及派生类特殊性的成员只定义在基类
对于想要派生类自定义的成员函数,使用 virtual 关键字声明函数为虚函数
通过类派生列表明确指出它从哪个基类继承而来

  • 派生类必须对所有重新定义的虚函数进行声明(如果不重新定义不需要声明可以直接用基类的)
  • 可以在这些函数前加上 virtual 关键字

动态绑定

关键在于派生类对象包含有基类的子对象,故派生类可以类型转换到基类(截取)
所以指向基类的指针可以指向派生类的子对象部分,这就是动态绑定

定义基类和派生类

定义基类

1
2
3
4
5
6
7
8
9
10
11
12
13
class Quoto {
public:
Quoto() = default;
Quoto(const std::string &book, double sales_price):
bookNo(book), price(sales_price) { }
std::string isbn() const { return bookNo; }
virtual double net_price(std::size_t n) const {return n * price; }
virtual ~Quote() = default; // 基类通常会定义虚析构函数
private:
std::string bookNo;
protected:
double price = 0.0;
}

成员函数与继承
基类希望其派生类进行覆盖的函数

  • 将其定义为虚函数,用 virtual 声明
    • 关键字 virtual 只能出现在类内部的声明语句之前而不能用于类外部的函数定义
  • 当使用指针或引用调用虚函数时,该调用将被动态绑定,根据指针绑定的对象类型不同选择不同的函数
  • 任何构造函数之外的非静态函数都可以是虚函数
  • 如果基类把一个函数声明为虚函数,则该函数在派生类中隐式地也是虚函数
    成员函数如果没有被声明为虚函数,则其解析过程发生在编译时而非运行时

访问控制和继承
派生类可以继承定义在基类中的成员,但是派生类的成员函数不一定有权访问从基类继承而来的成员。
派生类继承基类是产生了一个基类的子对象,对基类的访问,可以类比对象的API调用
protected是例外

定义派生类

1
2
3
4
5
6
7
8
9
10
11
12
class Bulk_quote : public Quote { // 通过类派生列表明确指出它从哪个基类继承而来
// 访问说明符的作用:控制派生类从基类继承而来的成员是否对派生类的用户可见
// 公有派生:表示基类的公有成员也是派生类接口的组成部分
// 能够将公有派生类型的对象绑定到基类的引用或指针上
public:
Bulk_quote() = default;
Bulk_quote(const std::string&, double, std::size_t, double);
double net_price(std::size_t n) const override; // 对于需要覆盖的重新声明
private:
std::size_t min_qty = 0;
double discount = 0.0;
}

派生类中的虚函数
如果派生类没有覆盖其基类的虚函数 则派生类直接继承基类的版本
推荐使用 override 关键字告知编译器这个是虚函数重写,否则你可能自己重新定义一个函数,虽然不会报错,但不是重写

派生类对象及派生类向基类的类型转换
一个派生类对象包含多个组成部分

  • 派生类自己定义的(非静态)成员的子对象
  • 派生类继承的基类对应的子对象
  • 如果有多个基类,子对象也有多个

    C++标准没有明确规定派生类的对象在内存中如何分布

因为派生类对象中含有与其基类对应的组成部分,所以我们能把派生类的对象当成基类对象来使用

1
2
3
4
Quote item;
Bulk_quote bulk;
Quote *p = &item;
p = &bulk; // 派生类到基类的类型转换 p指向bulk的Quote部分

派生类构造函数
派生类使用基类的构造函数来初始化它的基类部分
首先初始化基类部分,然后按照声明的顺序依次初始化派生类的成员

1
2
Bulk_quote(const std::string& book, double p, std::size_t qty, double disc):
Quote(book, p), min_qty(qty), discount(disc) { }

派生类使用基类的成员
public派生类可以访问基类的公有成员和受保护成员

继承和静态成员
如果基类定义了一个静态成员,则在整个继承体系中只存在该成员的唯一定义

派生类的声明
包含类名,但不包含它的派生列表

1
class Bulk_quote;

被用作基类的类
如果要将某个类用作基类,则该类必须已经定义而非仅仅声明

防止继承的发生
final 关键字可以防止继承

1
2
3
class NoDerived final {
/* */
};

类型转换与继承

当使用基类的引用或指针时,不清楚该引用或指针所绑定的对象是基类还是他派生类

静态类型和动态类型
静态类型:变量声明时的类型或表达式生成的类型
动态类型:变量或表达式表示的内存中的对象的类型
基类的指针或引用的静态类型可能与其动态类型不一致

不存在基类向派生类的隐式类型转换

对象之间不存在隐式类型转换

虚函数

第十六章 模板与泛型编程

类模板和函数模板在编译时实例化

16.1 定义模板

函数模板

1 什么是函数模板?以代码举例(举一种例子)

  • 函数模板需要以 template 开始,后跟一个模板参数列表
1
2
3
4
template <typename T>
void func(T& x) {
cout << x;
}

2 函数模板的实例化

  • 编译器推断出模板参数
  • 如果代码中使用 func(5) ,则会在编译时将 func 实例化为 int 对应的类型

3 函数模板的定义必须要在头文件里面

  • 如果函数模板的只在头文件 template.h 声明,在 template.cpp 定义
  • 在独立编译时, main.cpp 会生成对模板函数实例化的调用 func<int>()template.cpp 由于没有任何实例化的调用,不会编译出 func<int>() 对应的实例定义
  • 在链接时, main.cpp 找不到 func<int>() ,会出现如下报错
1
2
3
/usr/bin/ld: /tmp/ccZRz9CH.o: in function `main':
test.cpp:(.text+0xe): undefined reference to `void func<int>(int)'
collect2: error: ld returned 1 exit status

4 函数模板的模板参数可以是类型,也可以是值

  • 模板类型参数( type parameter ),使用 classtypename 说明
  • 非类型模板参数( nontype parameter )
  • 不管是什么参数,必须能够在编译时确定,否则链接时就找不到对应

5 inlineconstexpr 的函数模板
这些要放在模板参数列表之后

6 模板程序应该尽量减少对实参类型的要求

7 模板的设计者需要保证

  • 提供一个头文件:包括模板定义、用到的所有名字的声明

类模板

1 类模板和模板类的区别
类模板是指一个类型的模板,不是一个类型
实例化之后是一个类——模板类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
template <typename T> // template 后跟模板参数列表
class MyClass {
public:
T data;
MyClass(T val) : data(val) {}
void display() { // 内部定义的成员函数
std::cout << "Value: " << data << std::endl;
}
};

// 模板类是类模板的实例化:
// 编译器不能为类模板推断模板参数类型 需要显示模板实参 <int>
MyClass<int> intInstance(10); // 模板类,类型为 MyClass<int>
MyClass<double> doubleInstance(3.14); // 模板类,类型为 MyClass<double>

2 外部定义类模板的成员函数

  • 需要 template 关键字且后面跟着模板参数列表
  • 需要在模板名后面加上模板实参来为函数说明作用域,即 Myclass<T> ,否则就不能实例化了
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
template <typename T>
class MyClass {
public:
MyClass(T value); // 构造函数声明
void printValue(); // 成员函数声明
private:
T data;
};
// 在类外部定义构造函数
template <typename T>
MyClass<T>::MyClass(T value) : data(value) {}
// 在类外部定义成员函数
template <typename T>
void MyClass<T>::printValue() {
std::cout << "Value: " << data << std::endl;
}

3 默认情况下,实例化得到的模板类,它的成员只有在使用时才被实例化
这种特性可以让类模板用不完全符号模板操作的类型来实例化

4 在类模板自己的作用域中,可以直接使用模板名而不提供形参

  • 下面的 1 2 不需要提供形参
  • 下面的 3 需要提供形参
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 定义一个类模板
template <typename T>
class MyClass {
public:
MyClass(T value); // 构造函数声明
void printValue(); // 成员函数声明
// 在类内部直接使用模板名而不提供实参
MyClass createCopy() const; // 返回当前类型的副本 1 这里不需要提供形参
private:
T data;
};
// 在类外部定义构造函数
template <typename T>
MyClass<T>::MyClass(T value) : data(value) {} // 3 需要提供形参
// 在类外部定义成员函数
template <typename T>
void MyClass<T>::printValue() {
std::cout << "Value: " << data << std::endl;
}
// 在类外部定义 createCopy 成员函数
template <typename T>
MyClass<T> MyClass<T>::createCopy() const {
return MyClass(data); // 直接使用模板名 MyClass 2 这里不需要提供形参
}

5 类模板和友元

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
// 友元含有模板参数,指定类只能被指定类的友元访问
template <typename U>
class B; // 前向声明
template <typename T>
class A {
private:
T secret;
friend class B<T>; // 友元声明:只有 B<T> 能访问当前 A<T> 的私有成员
public:
A(T val) : secret(val) {}
};
template <typename U>
class B {
public:
void peek(const A<U>& a) {
// ✅ 允许访问:当 A<int> 与 B<int> 类型匹配时
std::cout << "B<U> sees secret: " << a.secret << std::endl;
}
};

int main() {
A<int> a_int(42);
B<int> b_int;
b_int.peek(a_int); // 正常工作

// B<double> b_double;
// b_double.peek(a_int); // ❌ 编译错误:B<double> 无法访问 A<int> 的私有成员
}
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
33
34
// 友元不含模板参数,可以访问所有的实例
template <typename T>
class A {
private:
T secret;
template <typename U>
friend class B; // 友元声明:所有 B 的实例都可以访问 A 的任意实例的私有成员
public:
A(T val) : secret(val) {}
};
template <typename U>
class B {
public:
void peek_any(const A<U>& a) {
// ✅ 允许访问:无论 B 的模板参数是否匹配 A 的类型
std::cout << "B<U> sees secret: " << a.secret << std::endl;
}
template <typename V>
void peek_cross(const A<V>& a) {
// ✅ 允许访问:B 可以操作任意 A<V> 的实例
std::cout << "B<U> also sees cross-secret: " << a.secret << std::endl;
}
};
int main() {
A<int> a_int(42);
A<double> a_double(3.14);

B<int> b_int;
b_int.peek_any(a_int); // ✅
b_int.peek_cross(a_double); // ✅

B<std::string> b_str;
b_str.peek_cross(a_int); // ✅
}

6 一些常见的友元

1
2
3
4
template <typename T> class Pal;
class C {
friend class Pal<C>; // 用类 C 实例化的 Pal 是 C 的一个友元
};
1
2
3
4
template <typename T> class Pal;
class C {
template <typename T> friend class Pal2; // 普通类与所有模板实例的友元关系
};
1
2
3
4
5
template <typename T> class C2 {
friend class Pal<T>; // C2 的每个实例将相同实例化的 Pal 声明为友元
template <typename X> friend class Pal2; // 模板类与所有模板实例的友元关系
friend class Pal3; // 模板类与普通类的友元关系
};
1
2
3
template <typename T> class A {
friend T; // T 成为 A<T> 的友元
};

7 类模板的别名 C++11

1
2
template <typename T> using twin = pair<T, T>;
twin<string> authors; // 等价于 pair<string, string> authors;

8 类模板的 static 成员

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
33
34
35
#include <iostream>

template<typename T>
class Counter {
public:
static int count; // 声明静态成员

Counter() { ++count; }
Counter(const Counter&) { ++count; }
~Counter() { --count; }

static void show() {
std::cout << "Count for " << typeid(T).name()
<< ": " << count << "\n";
}
};

// 定义静态成员(必须在头文件中)
template<typename T>
int Counter<T>::count = 0;

int main() {
Counter<int> a; // int型计数器+1
Counter<int> b; // int型计数器+1 → 2
Counter<double> c; // double型计数器+1 → 1

{ // 新的作用域
Counter<int> d(a); // 拷贝构造,int型+1 → 3
Counter<double> e; // double型+1 → 2
Counter<int>::show(); // 输出3
} // e和d析构,int型→2,double型→1

Counter<int>::show(); // 输出2
Counter<double>::show(); // 输出1
}

模板参数

1 模板参数的作用域

1
2
3
4
typedef double A;
template <typename A, typename B> void f(A a, B b) {
A tmp = a; // 此处 A 为模板参数
}

2 模板声明必须包含模板参数
声明中的模板参数名字不必与定义中相同

3 typename 显示告诉编译器 size_type 是一个类型
下面的 size_type 可以是类型也可以是静态成员变量
默认情况下, C++ 假定通过作用域访问的名字为变量

1
2
3
4
5
6
7
8
9
10
template <typename T>
class Container {
public:
using size_type = T; // 定义了一个类型成员
};

class MyClass {
public:
static int size_type; // `size_type` 是一个静态成员变量
};

4 默认模板实参

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 参数顺序:必须从右往左设置默认值
template<typename T = int, // 类型默认int
typename Container = std::vector<T>, // 容器默认vector<T>
typename Comparator = std::less<T>> // 比较器默认less<T>
class SortedCollection {
private:
Container data;
Comparator comp;

public:
void insert(const T& val) {
data.push_back(val);
// 伪排序示意(实际需实现排序逻辑)
std::sort(data.begin(), data.end(), comp);
}
};