C++概述
C++特点: C++ 是一门多范式的通用编程语言。
C++支持”面向对象编程”、“泛型编程”、”函数式编程”:
- 面向对象:核心在于抽象和封装,通过高内聚低耦合的对象之间的通信协作来完成任务。
- 泛型编程:核心是“一切皆为类型”,通过模板而不是继承的方式来复用代码。
- 函数式编程:核心是“一切皆可调用”,通过一系列连续或者嵌套的函数调用来实现对数据的处理。
生命周期和编程范式
一个 C++ 程序从“诞生”到“消亡”,要经历这么几个阶段:编码(Coding)、预处理(Pre-processing)、编译(Compiling)和运行(Running)。
预处理:
Pre-processor
执行,目的是文字替换。预处理指令都以符号#开头,应用于#include、#define、#if。#define宏定义,本身没有作用域概念,为全局生效。编译:完成的工作是”编译“和”链接“,生成计算机可识别的机器码。常用的特性为:“属性”和“静态断言”,
数据类型与关键字
- const const定义的变量只能为整数或者枚举(#define宏定义没有数据类型只是简单的字符替换,无法进行类型检查)。const的作用是能
防止修改,增加程序健壮性
,节省空间,避免不必要的内存分配
,因为const定义常量从汇编的角度来看,只是给出了内存地址,而不是像#define一样给出的立即数,所以#define存在若干拷贝,而const不会。
非const变量默认为extern,而const对象默认为文件局部变量,如果需要访问,则需要显式声明extern,且需要初始化。const与指针:
如果const位于*的左侧,则const就是用来修饰指针所指向的变量,即指针指向为常量;如果const位于*的右侧,const就是修饰指针本身,即指针本身是常量。
1
2
3
4
const char * a; //指向const对象的指针或者说指向常量的指针。
char const * a; //同上
char * const a; //指向类型对象的const指针。或者说常指针、const指针。
const char * const a; //指向const对象的const指针。
volatile 其含义是“不稳定的”、”易变的“,当
volatile
对const
变量进行修饰后,编译器在生成二进制机器码的时候,不会做有副作用的优化:即编译器不会把const value
做常量替换,而是对内存取值。volatile
会禁止编译器做优化,因此除非必要,应当少用volatile。mutable 修饰成员变量,允许
const成员函数
修改,mutable
变量的变化不影响对象的常量性,但是可能会“隐秘”地修改/损坏对象,尽量少用。
内存管理
基础概念
堆(heap):
指的是动态分配内存的区域,内存被分配之后需要手动释放,否则会造成内存泄漏。涉及两种分配/释放方式:
new
和delete
操作的区域为free store
,一般为堆的一个子集。malloc
和free
操作的区域为heap
。
1
2
// C++
auto ptr = new std::vector<int>();
实际内存分配过程中,分配内存要考虑程序当前已经有多少未分配的内存。内存不足时要从操作系统申请新的内存。释放内存不只是简单地把内存标记为未使用。对于连续未使用的内存块,通常内存管理器需要将其合并成一块,以便可以满足后续的较大内存分配要求。
在不考虑垃圾回收情况下,内存需要手动释放,且内存存在碎片化的情况下(释放内存导致非连续内存无法被合并)。
栈(stack):
在内存管理的语境下,指的是函数调用过程中产生的本地变量和调用数据的区域。这个栈和数据结构里的栈高度相似,都满足“后进先出”( last-in-first-out
或 LIFO
)。
C++函数调用过程中,本地变量便是使用栈管理内存:任何一个函数,根据架构的约定,只能使用进入函数时栈指针向上部分的栈空间。当函数调用另外一个函数时,会把参数也压入栈里(我们此处忽略使用寄存器传递参数的情况),然后把下一行汇编指令的地址压入栈,并跳转到新的函数。新的函数进入后,首先做一些必须的保存工作,然后会调整栈指针,分配出本地变量所需的空间,随后执行函数中的代码,并在执行完毕之后,根据调用者压入栈的地址,返回到调用者未执行的代码中继续执行。本地变量所需的内存就在栈上,跟函数执行所需的其他数据在一起。当函数执行完成之后,这些内存也就自然而然释放掉了。
栈的特点:
栈上的分配极为简单,移动一下栈指针而已。
栈上的释放也极为简单,函数执行结束时移动一下栈指针即可。
由于后进先出的执行过程,不可能出现内存碎片。数据往往是容量确定大小的。
RAII(Resource Acquisition Is Initialization):
RAII
,全称“资源获取即初始化”,对 RAII
的使用,使得 C++ 不需要类似于 Java 那样的垃圾收集方法,也能有效地对内存进行管理。其核心思想是:将资源(如内存、文件句柄、锁等)的获取和释放绑定到对象的生命周期。
RAII
核心机制:
资源获取:在对象的构造函数中获取资源。
资源释放:在对象的析构函数中释放资源。
作用域管理:资源的生命周期由对象的作用域控制,当对象离开作用域时,其析构函数自动被调用,资源被正确释放。
RAII
的常见应用:
资源类型 | RAII实现方式 | 标准实现 |
动态内存 | 智能指针(std::uniqueptr, std::sharedptr) | C++ 标准库支持 |
文件句柄 | 自定义文件管理 | std::fstream |
线程锁 | std::lockguard, std::uniquelock | C++ 标准库支持 |
数据库连接 | 数据库连接类 | 第三方库支持 |
网络资源 | 套接字管理类 | 第三方库支持 |
左值和右值
左值(lvalue)
左值是指具有 地址 的表达式,可以持久存在。
通常位于赋值运算符的左侧,但不绝对。
可以通过引用来操作。
特点: 可寻址:可以取得其地址(通过 & 操作符)/可修改(如果不是常量)。
右值(rvalue)
- 右值是 临时值,不能通过引用直接访问地址。
- 通常位于赋值运算符的右侧。
- 右值可能是字面值、临时变量、表达式的结果。
特点:不可寻址:无法通过 & 取得其地址(除非通过类型转换)/短暂:右值的生命周期通常很短,通常仅在表达式求值期间有效。
使用场景:
- 引用绑定
C++提供两种引用类型:左值引用和右值引用。
1
2
3
int a = 10;
int& lref = a; // 左值引用,绑定到变量 a
int&& rref = 20; // 右值引用,绑定到字面值 20
右值引用是绑定到右值的引用,通过 && 声明。它主要用于实现移动语义和完美转发(对应智能指针实现)。右值引用示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <iostream>
#include <utility> // std::move
using namespace std;
class MyClass {
public:
MyClass() { cout << "Default Constructor" << endl; }
MyClass(const MyClass&) { cout << "Copy Constructor" << endl; }
MyClass(MyClass&&) { cout << "Move Constructor" << endl; }
};
int main() {
MyClass obj1; // 默认构造函数
MyClass obj2 = obj1; // 拷贝构造函数
MyClass obj3 = std::move(obj1); // 移动构造函数
return 0;
}
C++引入了更精细的值类型划分:
值类别 | 含义 | 示例 |
左值(lvalue) | 持久存在的、可寻址的对象 | ++x、x = 1、cout « ’ ‘ 字符串 |
右值(rvalue) | 短暂存在的值 | 字面量、表达式结果等 |
纯右值(prvalue) | 只表示值本身,不关联存储位置 | x++、x + 1、makeshared |
亡值(xvalue) | 即将被销毁的对象(右值引用转移的结果),有标识符但无法被引用 | std::move(x) 的结果 |
指针
指针
源C语言,本质上是一个内存地址索引,代表一小片内存区域,支持直接读写内存;因为指针能够完全映射计算机硬件,效率高的同时也存在很多问题:访问无效数据、指针越界、内存无法释放等。
智能指针
基于 RAII
包装了裸指针,且重载了 *
和 ->
操作符,用起来和原始指针一致。
智能指针分三类: unique_ptr
、 shared_ptr
、 weak_ptr
,智能指针实际上是对象,自动管理初始化时的指针,离开作用域后自动释放内存,也不能调用delete,且未初始化的指针默认为空指针,建议用工厂函数初始化。
- uniqure_ptr
uniqure_ptr
需要基于C++的转移语义,同时禁止拷贝复制。 基本使用方法:
1
2
3
4
5
6
7
8
auto ptr1 = make_unique<int>(12);
assert(ptr1 && *ptr1 == 12);
auto ptr2 = make_unique<string>("God of war.");
assert(!ptr2->empty());
auto ptr3 = std::move(ptr2);
assert(!ptr2 && ptr3); // move转移控制权后,ptr2成为空指针
- shared_ptr
shared_ptr
和 uniqure_ptr
的区别在于:它的所有权是可以被安全共享的,即支持拷贝赋值,允许被多个人持有。 shared_ptr
支持安全共享的原理在于其内部使用了“引用计数”。 shared_ptr
的存储/管理成本更高,且存在循环引用的问题:
1
2
3
4
5
6
7
auto ptr1 = make_shared<int>(43);
assert(ptr1 && ptr1.unique()); // 此时指针有效且唯一
auto ptr2 = ptr1; // 支持直接拷贝赋值
assert (ptr1 == ptr2); // 支持直接比较
assert(!ptr1.unique() && ptr1.use_count() == 2); // 两个智能指针不唯一,且引用计数为2
- weak_ptr
weak_ptr
能够解决循环引用的问题,赋值时不会添加引用计数,为弱引用,使用的时候通过 loca()
函数获取 shared_ptr
强引用。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Node final
{
public:
std::weak_ptr<Node> next;
}
auto n1 = make_shaped<Node>();
auto n2 = make_shaped<Node>();
n1->next = n2;
n2->next = n1; // 循环引用,如果shared_ptr,则循环引用次数变为2
assert(n1.use_count() == 1); // weak_ptr,引用计数为1
if (!n1->next.expired()) { // 检查指针是否有效
auto ptr = n1->next.lock(); // lock获取shared_ptr
assert(ptr == n2);
}
C++类
类的基本概念:
- 类函数
- 构造函数 初始化类的对象,定义对象的初始化状态。函数分为:默认构造函数、参数化构造函数、拷贝构造函数、移动构造函数。
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
// 默认构造函数 class MyClass { public: MyClass() { /* 初始化 */ } // 用户自定义默认构造函数 }; // 参数化构造函数 class MyClass { public: MyClass(int value) : data(value) {} private: int data; }; // 拷贝构造函数 class MyClass { public: MyClass(const MyClass& other) { /* 拷贝数据 */ } }; // 移动构造函数 class MyClass { public: MyClass(MyClass&& other) noexcept { /* 转移资源 */ } };
析构函数 析构函数能用于清理类对象占用的资源,编译器默认生成析构函数,但如果需要涉及管理动态资源,需要显式定义。
拷贝赋值函数&&移动赋值函数 拷贝赋值函数完成一个对象赋值给另一个已经存在的对象,如果未显式定义,编译器会自动生成浅拷贝实现。
1 2 3 4 5 6 7
class MyClass { public: MyClass& operator=(const MyClass& other) { if (this != &other) { /* 防止自赋值 */ } return *this; } };
移动赋值构造函数,是将一个临时对象转移到另一个对象中,可显著提高性能,避免不必要的资源拷贝。
1 2 3 4 5 6 7
class MyClass { public: MyClass& operator=(MyClass&& other) noexcept { if (this != &other) { /* 转移资源 */ } return *this; } };
其他常用函数
- 友元函数:用于特定情况下,允许非成员函数访问私有数据。它不属于类,但可以访问类的私有和保护成员。由于它是非成员函数,所以不能通过 this 指针访问对象。使用友元函数的场景:
特定情况下需要访问私有成员:提供类的内部数据与外部函数之间的接口。避免通过增加公共成员函数来暴露私有数据。
运算符重载的便利:友元函数常用于运算符重载(如 « 和 »),因为这些运算符无法作为类成员函数实现。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
#include <iostream> class MyClass { private: int data; public: MyClass(int val) : data(val) {} // 声明友元函数 friend std::ostream& operator<<(std::ostream& os, const MyClass& obj); }; // 定义友元函数 std::ostream& operator<<(std::ostream& os, const MyClass& obj) { os << obj.data; return os; } int main() { MyClass obj(42); std::cout << obj << std::endl; // 输出: 42 return 0; }
constexpr构造: constexpr 构造函数是 C++11 引入的一种特殊构造函数,允许在编译时对对象进行初始化。它保证了构造函数是一个常量表达式,适用于编译期计算的场景。要求:构造函数的所有操作必须在编译器是可计算的,且数据成员必须能够在常量表达式中初始化。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
#include <iostream> class MyClass { private: int value; public: // constexpr 构造函数 constexpr MyClass(int v) : value(v) {} // constexpr 成员函数 constexpr int getValue() const { return value; } }; int main() { constexpr MyClass obj(42); // 编译期构造对象 constexpr int val = obj.getValue(); // 编译期获取值 std::cout << val << std::endl; // 输出: 42 return 0; }
委托构造函数 委托构造函数(Delegating Constructor)是 C++11 引入的一种功能,允许一个构造函数调用同一类中的另一个构造函数。目的是减少代码重复,简化构造函数的实现。
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
#include <iostream> class MyClass { private: int value1; int value2; public: // 主构造函数 MyClass(int v1, int v2) : value1(v1), value2(v2) {} // 委托构造函数 MyClass(int v) : MyClass(v, 0) {} // 调用主构造函数 void display() const { std::cout << "Value1: " << value1 << ", Value2: " << value2 << std::endl; } }; int main() { MyClass obj1(10, 20); obj1.display(); // 输出: Value1: 10, Value2: 20 MyClass obj2(42); // 调用委托构造函数 obj2.display(); // 输出: Value1: 42, Value2: 0 return 0; }
- 重载运算符: 运算符重载允许开发者为用户定义的类型(如类和结构体)赋予类似内置类型的操作能力。这是通过定义特殊的运算符函数实现的。一些注意事项:
- 必须使用类的成员函数: 一元运算符(如 +)和赋值运算符必须是类的成员函数。
- 可以使用友元函数: 如 « 和 » 通常实现为友元函数。
- 不要滥用运算符重载: 确保重载运算符的语义符合预期。
- 友元函数:用于特定情况下,允许非成员函数访问私有数据。它不属于类,但可以访问类的私有和保护成员。由于它是非成员函数,所以不能通过 this 指针访问对象。使用友元函数的场景:
虚函数与多态 虚函数(virtual function)是 C++ 提供的一种机制,用于支持运行时多态(也称为动态多态)。它允许派生类覆盖基类的成员函数,并在运行时根据实际对象类型选择合适的函数调用。在基类中使用 virtual 修饰的成员函数就是虚函数,派生类可以重写(覆盖)它。C++ 编译器会为类生成一个虚函数表,用于在运行时根据对象的类型调用正确的函数。调用虚函数时,程序会在运行时确定调用哪个版本的函数(基类的版本或派生类的版本)。
虚函数的工作原理:
虚函数表(vtable):
- 每个含有虚函数的类都会生成一个虚函数表(vtable)。
- 表中存储指向该类所有虚函数的指针。
- 对象通过指针查找虚函数表,从而确定调用的函数。
优点:实现动态多态,简化代码设计。支持接口抽象,适合面向对象的设计模式。
缺点:运行时查找虚函数表有一定性能开销。增加类的内存开销:每个对象包含指向虚函数表的指针。增加了复杂性:需要注意析构函数是否为虚函数(防止基类函数指针删除派生类对象时,导致析构函数调用错误,引发内存泄漏)。
访问控制和特殊修饰符
修饰符 功能与应用 public 所有人都可访问 protected 本类和派生类可访问 private 仅本类可访问 const 成员函数不修改状态,变量或指针不可更改 static 类共享成员和成员函数,不依赖具体对象 virtual 支持动态多态 override 明确标识覆盖虚函数,防止签名错误 final 阻止函数被覆盖或类被继承 mutable 允许在常量成员函数中修改变量 explicit 防止隐式类型转换 inline 建议编译器将函数