Professional C++学习记录

Professional C++ 第六版学习记录

Github

基础知识

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 };

image

ptr只是指向了堆上的内存,本身仍在栈上。

注意:指针只是变量,可以存在于堆上或者栈上。而动态内存总是分配在堆上

下面代码先声明了一个指向整型指针的指针作为变量 handle,动态分配足够的内存来存储一个指向整型的指针,并将指向新内存的指针存储在 handle 中。接下来,内存 (*handle)分配了一个指向另一个足够大的动态内存区域的指针,该区域可以存储整数。

int** ptr {nullptr};
ptr = new int*;
*ptr = new int;
  • ptr类型是int **,指向一个int *,也就是指向int *的指针

  • *ptr类型是int *,保存一个指向int的地址

  • **ptr类型是int,表示最终的整型值

image

如图所示,先删除*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;
}

image

数组指针二象性

  • 数组会自动退化成指针。但并非所有的指针都是数组。

  • 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的虚函数表。

image

虚析构函数

析构函数应该为虚。如果析构函数不为虚,很容易导致对象销毁时内存没有释放。只有在标记为 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异常;

posted @ 2025-08-27 22:54  杰西卡若  阅读(22)  评论(0)    收藏  举报