Professional C++学习记录
Professional C++ 第六版学习记录
基础知识
const使用
const 用来表示不可修改的值或者对象
常量
const int a = 10; // a不可被修改
a = 12 // error
std::optional
-
类模板 std::optional 管理一个可选的包含值,即一个可能存在也可能不存在的值。
-
optional 的一个常见用例是作为可能失败的函数的返回值。与其他方法(例如 std::pair<T, bool>)不同,optional 能够很好地处理构造开销大的对象,并且更具可读性,因为它明确表达了意图。
-
在任何给定时间,任何 optional 实例要么包含一个值,要么不包含一个值。
-
optional 对象模拟一个对象,而不是一个指针。
// optional can be used as the return type of a factory that may fail
std::optional<std::string> create(bool b)
{
if (b)
return "Godzilla";
return {};
}
// std::nullopt can be used to create any (empty) std::optional
auto create2(bool b)
{
return b ? std::optional<std::string>{"Godzilla"} : std::nullopt;
}
int main()
{
std::cout << "create(false) returned " << create(false).value_or("empty") << '\n';
// optional-returning factory functions are usable as conditions of while and if
if (auto str = create2(true))
std::cout << "create2(true) returned " << *str << '\n';
}
结果:
create(false) returned empty
create2(true) returned Godzilla
内存管理
理解内存
下面的代码在栈上面创建了初始化为nullptr的变量ptr,然后为ptr指向堆上的内存。
int* ptr { nullptr };
ptr = new int; // 两段语句等价于int* ptr { new int };
ptr只是指向了堆上的内存,本身仍在栈上。
注意:指针只是变量,可以存在于堆上或者栈上。而动态内存总是分配在堆上
下面代码先声明了一个指向整型指针的指针作为变量 handle,动态分配足够的内存来存储一个指向整型的指针,并将指向新内存的指针存储在 handle 中。接下来,内存 (*handle
)分配了一个指向另一个足够大的动态内存区域的指针,该区域可以存储整数。
int** ptr {nullptr};
ptr = new int*;
*ptr = new int;
-
ptr
类型是int **
,指向一个int *
,也就是指向int *
的指针 -
*ptr
类型是int *
,保存一个指向int的地址 -
**ptr
类型是int,表示最终的整型值
如图所示,先删除*handle
,存放的是int
类型的地址,再删除*handle
,它指向的是int *
类型。
delete *handle; // 释放 0x3000 的整型数据
delete handle; // 释放 0x2000 的指针内存
handle = nullptr; // 安全重置
分配和释放
malloc&free和new&delete的主要区别
-
new不仅分配内存,还会构造对象
-
malloc只是为指定的大小分配了一块内存,不知道也不关心对象
-
delete会调用对象的析构函数,free不会调用
-
malloc&free是函数 new&delete是操作符
数组内存的分配与释放
class Simple
{
public:
Simple() {
std::cout << " Simple construct function..."<< std::endl;
}
~Simple() {
std::cout << " Simple destory construct function..."<< std::endl;
}
};
下面代码在栈上创建了一个Simple*
类型的变量mySimpleArray
它指向了在堆上的四块内存区域。
对象数组
Simple* mySimpleArray = new Simple[4];
delete[] mySimpleArray;
mySimpleArray = nullptr;
指针数组,每个指针指向了一个对象。
const std::size_t s = 4;
Simple** mySimpleArray1 = new Simple*[s];
for(int i = 0; i < s; ++i) {
mySimpleArray1[i] = new Simple(); // 存储Simple对象
}
for(int i = 0; i < s; ++i)
{
delete mySimpleArray1[i];
mySimpleArray1[i] = nullptr; // 循环置空
}
delete[] mySimpleArray1;
mySimpleArray1 = nullptr;
二维数组
char** board = new char*[size];
char ** allocateCharacterBoard(size_t xDimension , size_t yDimension)
{
char ** myArray = char* [xDimension];
for(int i = 0; i < xDimension; ++i)
{
myArray[i] = new char[yDimension];
}
return myArray;
}
数组指针二象性
-
数组会自动退化成指针。但并非所有的指针都是数组。
-
int *ptr = {new int};
内存泄漏
内存分配后没有及时释放,比如创建一个对象
Simple * originPoint = new Simple();
调用函数,当你delete originPoint的时候,删除的是outsimplePoint而不是originPoint。
void func(Simple * &outsimplePoint) { outsimplePoint = new Simple(); }
双重释放
当一个指针被释放之后。该指针关联的内存,就有可能在程序其他地方使用,现在它是一个悬空指针,当你再次释放该指针的时候,就有可能释放掉程序其他的地方的内存。
正确做法:应该在释放指针之后,将它置为空
智能指针
weak_ptr
1.weak_ptr
不拥有资源,用来确定关联的shared_ptr
资源是否释放。
2.注意:
-
使用
weak_ptr
实例上的 lock() 成员函数,该函数返回一个shared_ptr
。如果在此期间关联的shared_ptr
已经释放,返回的shared_ptr
将为 nullptr。 -
创建一个新的
shared_ptr
实例,并将weak_ptr
作为shared_ptr
构造函数的参数传递。如果关联的shared_ptr
已经释放,这将抛出std::bad_weak_ptr
异常 -
要访问对象,必须将
weak_ptr
转换为shared_ptr
(使用lock()
成员函数),如果对象还存在,则获得一个有效的shared_ptr
,否则返回空的shared_ptr
。
解决循环引用问题
class Child;
class Parent
{
public:
std::shared_ptr<Child> child;
~Parent();
};
class Child
{
public:
std::shared_ptr<Parent> parent;
~Child();
};
int main()
{
auto parent = std::make_shared<Parent>();
auto child = std::make_shared<Child>();
parent->child = child;
child->parent = parent; // 循环引用
}
// 只需要
class Child
{
public:
std::weak_ptr<Parent> parent;
~Child();
};
类和对象
构造函数和拷贝构造和移动构造
默认构造:创建一个对象,没有任何参数.
MyClass obj;
拷贝构造:用一个已经存在的对象初始化一个新的对象。
MyClass obj;
MyMyClass obj2 = obj;
赋值运算符:用一个对象的值赋给另一个已存在的对象时
MyClass obj;
MyMyClass obj2;
obj2 = obj;
移动构造:用一个右值(通常是临时对象)初始化一个新对象时
MyClass obj = MyClass();
MyClass obj(std::move(obj2))
移动赋值运算符:将一个右值对象的值赋给另一个已存在的对象时
Obj1 = std::move(obj2);
拷贝赋值和移动赋值
Person& operator=(Person&& p) noexcept
Person& operator=(const Person& p) noexcept
拷贝赋值:把另一个对象的内容复制到 *this
,源对象保持不变(语义上)。
移动赋值:把另一个对象的资源窃取/接管到 *this
,源对象被置为“已搬走(moved-from)”的可用但未规定状态,从而避免昂贵的复制。
继承
静态绑定(早期绑定)
在C++中编译一个类时,会创建一个二进制对象,其中包含类的所有成员函数。函数非虚的时候,根据编译时类型,直接在调用成员函数地方硬编码。
虚函数表
有一个或多个虚函数的类都有一个虚函数表。该类的每个实例都包含一个指向该vtable的指针。
动态绑定(晚期绑定)
vtable包含指向虚成员函数实现的指针,当在指向或引用对象的指针或引用上调用成员函数时,会跟随其vtable指针,并根据运行时对象的实际类型调用对应的成员函数。
class Base
{
public:
virtual void func1();
virtual void func2();
void test();
};
class Derived : public Base
{
public:
void func2() override;
void test();
};
如图所示,Base有两个虚函数,和一个成员函数。Derived继承Base并重写了func2()函数。当Derived对象调用func2()函数时,myDerived的虚函数指针指向了Derived的虚函数表,而func1()并没有被重写,仍指向Base的虚函数表。
虚析构函数
析构函数应该为虚。如果析构函数不为虚,很容易导致对象销毁时内存没有释放。只有在标记为 final 的类中,才可以让其析构函数设置为非虚。
```c++
class Base
{
public:
virtual ~Base();
};
class Derived : public Base
{
public:
~Derived();
};
基类析构函数声明为虚函数,根据虚函数表,从派生类到基类的顺序一次调用析构函数。如果不是,析构函数的选择编译时决定的。
构造顺序
基类->类的非静态数据成员->类
public Something
{
public:
Something() { std::cout << "2\n"; }
};
class Base
{
public:
Base() { std::cout << "1\n"; }
};
class Derived : public Base
{
public:
Derived() { std::cout << "3\n"; }
private:
Something _some;
};
结果:1,2,3
析构顺序
类->以与构造相反的顺序销毁类的数据成员->基类
上下转换
派生类转基类
会丢失自己所有的特性,只包含父类的特性。会产生切片。
Derived myDerived;
Base myBase { myDerived}; // 产生切片
Base &myBase { myDerived}; // 不产生切片
基类转派生类
这是一种非常不好的设计。尽量使用 dynamic_cast()。
使用对象的内置类型来拒绝不合理的转换。这种内置类型位于vtable上,所以dynamic_cast只对具有vtable的对象有效。
在指针上转换失败,返回nullptr;
在对象引用上转换失败,抛出std::bad_cast异常;