C++Primer笔记
第十二章 动态内存
全局对象:程序启动时分配,程序结束时销毁
局部自动对象:进入其定义所在程序块时创建,离开块时销毁
局部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 | shared_ptr<string> 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 |
|
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_ptr
和 new
结合使用
- 可以用
new
返回的指针来初始化智能指针 - 但不能将内置指针隐式转换为一个智能指针,必须使用直接初始化形式来初始化一个智能指针
- 用来初始化智能指针的普通指针必须指向动态内存,因为智能指针会默认
delete
释放它所关联的对象
17 不要混合使用普通指针和智能指针
- 将同一块内存绑定到多个独立创建的
shared_ptr
会导致智能指针多次析构释放同一块内存 - 将普通指针绑定到
shared_ptr
之后,当shared_ptr
析构了完,普通指针指向的动态内存被释放了
18 不要用 get
初始化另一个智能指针或为智能指针赋值
会出现上条的类似情况,动态内存被新的智能指针释放
19 reset
1 | shared_ptr<int> p = make_shared<int>(); |
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 | class Quoto { |
成员函数与继承
基类希望其派生类进行覆盖的函数
- 将其定义为虚函数,用
virtual
声明- 关键字
virtual
只能出现在类内部的声明语句之前而不能用于类外部的函数定义
- 关键字
- 当使用指针或引用调用虚函数时,该调用将被动态绑定,根据指针绑定的对象类型不同选择不同的函数
- 任何构造函数之外的非静态函数都可以是虚函数
- 如果基类把一个函数声明为虚函数,则该函数在派生类中隐式地也是虚函数
成员函数如果没有被声明为虚函数,则其解析过程发生在编译时而非运行时
访问控制和继承
派生类可以继承定义在基类中的成员,但是派生类的成员函数不一定有权访问从基类继承而来的成员。
派生类继承基类是产生了一个基类的子对象,对基类的访问,可以类比对象的API调用
protected是例外
定义派生类
1 | class Bulk_quote : public Quote { // 通过类派生列表明确指出它从哪个基类继承而来 |
派生类中的虚函数
如果派生类没有覆盖其基类的虚函数 则派生类直接继承基类的版本
推荐使用 override
关键字告知编译器这个是虚函数重写,否则你可能自己重新定义一个函数,虽然不会报错,但不是重写
派生类对象及派生类向基类的类型转换
一个派生类对象包含多个组成部分
- 派生类自己定义的(非静态)成员的子对象
- 派生类继承的基类对应的子对象
- 如果有多个基类,子对象也有多个
C++标准没有明确规定派生类的对象在内存中如何分布
因为派生类对象中含有与其基类对应的组成部分,所以我们能把派生类的对象当成基类对象来使用
1 | Quote item; |
派生类构造函数
派生类使用基类的构造函数来初始化它的基类部分
首先初始化基类部分,然后按照声明的顺序依次初始化派生类的成员
1 | Bulk_quote(const std::string& book, double p, std::size_t qty, double disc): |
派生类使用基类的成员
public派生类可以访问基类的公有成员和受保护成员
继承和静态成员
如果基类定义了一个静态成员,则在整个继承体系中只存在该成员的唯一定义
派生类的声明
包含类名,但不包含它的派生列表
1 | class Bulk_quote; |
被用作基类的类
如果要将某个类用作基类,则该类必须已经定义而非仅仅声明
防止继承的发生final
关键字可以防止继承
1 | class NoDerived final { |
类型转换与继承
当使用基类的引用或指针时,不清楚该引用或指针所绑定的对象是基类还是他派生类
静态类型和动态类型
静态类型:变量声明时的类型或表达式生成的类型
动态类型:变量或表达式表示的内存中的对象的类型
基类的指针或引用的静态类型可能与其动态类型不一致
不存在基类向派生类的隐式类型转换
对象之间不存在隐式类型转换
虚函数
第十六章 模板与泛型编程
类模板和函数模板在编译时实例化
16.1 定义模板
函数模板
1 什么是函数模板?以代码举例(举一种例子)
- 函数模板需要以
template
开始,后跟一个模板参数列表
1 | template <typename T> |
2 函数模板的实例化
- 编译器推断出模板参数
- 如果代码中使用
func(5)
,则会在编译时将func
实例化为int
对应的类型
3 函数模板的定义必须要在头文件里面
- 如果函数模板的只在头文件
template.h
声明,在template.cpp
定义 - 在独立编译时,
main.cpp
会生成对模板函数实例化的调用func<int>()
,template.cpp
由于没有任何实例化的调用,不会编译出func<int>()
对应的实例定义 - 在链接时,
main.cpp
找不到func<int>()
,会出现如下报错
1 | /usr/bin/ld: /tmp/ccZRz9CH.o: in function `main': |
4 函数模板的模板参数可以是类型,也可以是值
- 模板类型参数( type parameter ),使用
class
或typename
说明 - 非类型模板参数( nontype parameter )
- 不管是什么参数,必须能够在编译时确定,否则链接时就找不到对应
5 inline
和 constexpr
的函数模板
这些要放在模板参数列表之后
6 模板程序应该尽量减少对实参类型的要求
7 模板的设计者需要保证
- 提供一个头文件:包括模板定义、用到的所有名字的声明
类模板
1 类模板和模板类的区别
类模板是指一个类型的模板,不是一个类型
实例化之后是一个类——模板类
1 | template <typename T> // template 后跟模板参数列表 |
2 外部定义类模板的成员函数
- 需要
template
关键字且后面跟着模板参数列表 - 需要在模板名后面加上模板实参来为函数说明作用域,即
Myclass<T>
,否则就不能实例化了
1 | template <typename T> |
3 默认情况下,实例化得到的模板类,它的成员只有在使用时才被实例化
这种特性可以让类模板用不完全符号模板操作的类型来实例化
4 在类模板自己的作用域中,可以直接使用模板名而不提供形参
- 下面的 1 2 不需要提供形参
- 下面的 3 需要提供形参
1 | // 定义一个类模板 |
5 类模板和友元
1 | // 友元含有模板参数,指定类只能被指定类的友元访问 |
1 | // 友元不含模板参数,可以访问所有的实例 |
6 一些常见的友元
1 | template <typename T> class Pal; |
1 | template <typename T> class Pal; |
1 | template <typename T> class C2 { |
1 | template <typename T> class A { |
7 类模板的别名 C++11
1 | template <typename T> using twin = pair<T, T>; |
8 类模板的 static
成员
1 |
|
模板参数
1 模板参数的作用域
1 | typedef double A; |
2 模板声明必须包含模板参数
声明中的模板参数名字不必与定义中相同
3 typename
显示告诉编译器 size_type
是一个类型
下面的 size_type
可以是类型也可以是静态成员变量
默认情况下, C++
假定通过作用域访问的名字为变量
1 | template <typename T> |
4 默认模板实参
1 | // 参数顺序:必须从右往左设置默认值 |