一、面向对象编程
1、类的定义
成员变量:描述类的属性(状态)
成员函数:定义类的行为(方法)
访问限定符:public任何地方都可访问、private仅类内部可访问、protected类内部和派生类可访问(用于封装)
cpp
class Student {
public://共有成员,类外部可以访问
//成员函数
void setName(string n) { name = n; }
string getName() { return name; }
private://私有成员,类外部不能直接访问
//成员变量
string name;
int age;
};
2、构造函数
构造函数在创建对象时自动调用,用于初始化对象的成员变量。它的名称与类名相同,没有返回值。
特点:可以重载(一个类可以重载多个构造函数)
默认构造函数:无参构造函数。如果你没有定义任何构造函数,编译器自动生成一个空的构造函数。
cpp
class Student {
public:
//无参构造函数
Student() {
name = "Unknown";
age = 0;
cout << "无参构造函数被调用" << endl;
}
//有参构造函数(重载)
Student(string n, int a) {
name = n;
age = a;
cout << "有参构造函数被调用" << endl;
}
private:
string name;
int age;
};
3、赋值构造函数
拷贝构造函数是一种特殊的构造函数,它用同一个类的一个已有对象来创建并初始化另一个新对象。语法:类名(const 类名& other)。
默认行为:编译器会生成一个默认的拷贝构造函数,它执行浅拷贝(将other的成员变量逐个复制给新对象)。
自定义(深拷贝)的必要性:当类中有指针成员,并且它在构造函数中动态分配了内存时,浅拷贝会导致两个对象的指针指向同一块内存。这会导致析构时重复释放(程序崩溃)或一个对象修改了内存影响另一个对象。此时,你需要自定义拷贝构造函数,进行深拷贝,为新对象分配独立的内存并复制内容。
cpp
class Person {
public:
//构造函数,为name动态分配内存
Person(const char* n) {
name = new char[strlen(n) + 1];
strcpy(name, n);
cout << "构造函数被调用" << endl;
}
//拷贝构造函数(深拷贝)
Person(const Person& other) {
//1.为新对象分配独立内存
name = new char[strlen(other.name) + 1];
//2.复制内容
strcpy(name, other.name);
cout << "拷贝构造函数被调用" << endl;
}
//析构函数...
private:
char* name;
};
4、析构函数 动态创建对象new与delete
析构函数:在对象被销毁时自动调用,用于清理资源。它的名称是在类名前加~,没有返回值,也没有参数。一个类只能有一个析构函数。
new和delete:用于在堆(Heap)上动态分配和释放内存。
使用new动态创建的对象,需要使用delete来手动销毁,否则会造成内存泄漏。|
在构造函数中用new分配了资源(比如上面的char* name),必须在析构函数中用delete来释放。
cpp
class Person {
public:
Person(const char* n) {
name = new char[strlen(n) + 1];
strcpy(name, n);
}
//析构函数
~Person() {
delete[] name;//对象销毁时,释放掉构造函数中动态分配的内存
cout << "析构函数被调用,已释放资源" << endl;
}
private:
char* name;
};
5、动态创建对象数组与普通数组
普通数组:int arr[10];在栈上分配,大小必须为编译时常量,生命周期在其使用域结束后自动结束。
动态数组:int* arr = new int[10];在堆上分配,大小可以是运行时可变的变量(如int size = 10; int * arr = new int[size];)。
必须用delete[] arr;来释放内存。
cpp
int main() {
int size = 10;//运行时变量
//创建动态数组
int* dynamicArr = new int[size];
//动态创建对象数组
Student* classRoom = new Student[30];
//使用数组
//释放动态数组的内存
delete[] dynamicArr;
delete[] classRoom;
return 0;
}
6、静态成员变量与方法
静态成员属于类本身,而不属于某个具体的对象。所有该类的对象共享同一个静态成员。
静态成员变量:
需要在类内声明,类外单独定义和初始化(通常放在.cpp文件中)。
不能在构造函数中初始化,因为它是共享的,会被多次赋值。
静态成员函数:
只能访问静态成员变量,不能访问非静态成员,因为它没有this指针
可以通过类名::函数名直接调用,无需通过对象。
cpp
class Counter {
public:
Counter() { count++; }
static int getCount() { return count; }//静态成员函数
private:
static int count;//静态成员变量声明
};
//静态成员变量定义和初始化(必须在类外)
int Counter::count = 0;
int main() {
Counter c1, c2, c3;
//通过类名直接调用静态函数
cout << "对象总数: " << Counter::getCount() << endl;//输出3
}
7、常成员变量函数
在成员函数的参数列表后加上const关键字,就是常成员函数。它是一种只读函数,承诺不会修改任何成员变量(mutable修饰的除外)。
const对象只能调用const成员函数
这是一种很好的设计契约,保证了对象在调用此函数后状态不变。
cpp
class Data {
public:
void setValue(int v) { value = v; } //普通成员函数
int getValue() const { return value; }//常成员函数,只读
private:
int value;
};
int main() {
const Data constObj;//常量对象
constObj.getValue(); //可以调用,因为getValue是const函数
//constObj.getValue(10); 错误!不能调用非常量成员函数
return 0;
}
8、this指针
this是一个在非静态成员函数内部隐式使用的指针,它指向调用该函数的当前对象本身。它的作用是指向成员函数所作用的对象。
注意区分同名参数和成员变量。返回对象自身,以实现链式调用。
cpp
class Person {
public:
Person& setAge(int age) {
this->age = age;//this->age 是成员变量,右边的age是参数
return *this;//返回对象自身
}
private:
int age;
};
int main() {
Person p;
p.setAge(20);//this指向p
return 0;
}
9、友元
友元是一种突破封装的机制。它允许一个外部函数或另一个类的成员函数访问当前类的private和protected成员。
友元函数:不是类的成员函数,可以是一个普通的外部函数。
友元类:如果class A声明class B为友元类,则B的所有成员函数都可以访问A的私有成员。
特性:单向的(A是B的友元,不代表B是A的友元)、不可传递(A是B的友元,B是C的友元,不代表A是C的友元)。
使用建议:友元破坏了封装性,增加了代码的耦合度,不宜滥用。
cpp
class A {
//声明友元类B,B可以访问A的私有成员
friend class B;
//也可以声明一个普通函数为友元
friend void showA(const A& a);
private:
int secret = 10;
};
class B {
public:
void accessA(A& a) {
cout << "B访问A的私有成员:" << a.secret << endl;//合法
}
};
void showA(const A& a) {
cout << "友元函数访问A的私有成员: " << a.secret << endl;
}
10、运算符重载
运算符重载允许你为自定义类型(类)重新定义或扩展C++中已有运算符(如+、-、*、=等)的含义。它本质上是一种特殊的函数重载。
格式:返回值类型operator运算符(参数列表)
两种形式:
成员函数:左侧操作数作为当前对象(*this)。
友元函数:通常用于需要左侧操作数不是本类对象的情况。
不能重载的运算符有::和.*和.和?:等
cpp
class Complex {
public:
Complex(double r = 0, double i = 0) : real(r), imag(i) {}
//重载为成员函数:c1 + c2
Complex operator+(const Complex& other) const {
return Complex(real + other.real, imag + other.imag);
}
//重载为友元函数:int + c1(友元声明在类内)
friend Complex operator+(int x, const Complex& c) {
return Complex(x + c.real, c.imag);
}
private:
double real, imag;
};
int main() {
Complex c1(1, 2), c2(3, 4);
Complex c3 = c1 + c2; //调用成员函数 operator+
Complex c4 = 5 + c1; //调用友元函数 operator+
return 0;
}
11、继承和派生概念
继承允许创建一个新类(派生类)从一个已有的类(基类)获得其成员,从而实现代码的复用。
继承方式:class 派生类名:继承方式 基类名
三种继承方式:
public继承:基类的public成员在派生类中仍为public、protected仍为protected。最常用。
protected继承:基类的public和protected成员在派生类中都变为protected。
private继承:基类的public和protected成员在派生类中都变为private。
12、继承的构造和析构函数调用顺序
构造时:先调用基类的构造函数,再调用成员对象的构造函数,最后调用派生类自身的构造函数。
析构时:与构造顺序完全相反。
13、多继承与菱形问题(virtual虚继承)和using的使用
菱形问题(钻石问题):当一个派生类D的两个基类B和C都继承自同一个基类A时,D会包含两份A的成员,导致二义性和数据冗余。
解决:虚继承(virtual):在中间层B和C继承A时,使用virtual关键字,这样D中就只有一份A的成员了。
cpp
class A { public: int data; };
class B : virtual public A {};//虚继承
class C : virtual public A {};//虚继承
class D : public B, public C {};//D中只有一份A::data
using的使用:在继承中,using可以用来修改基类成员在派生类中的访问权限,特别是可以让派生类访问基类的private成员(不推荐),或者将基类的protected/public成员在派生类中进一步公开。
14、多态概念 多态构成条件
多态是指同一个函数名,在不同对象中具有不同的行为。
静态多态(编译时):通过函数重载和运算符重载实现。
动态多态(运行时):通过继承和虚函数实现。这是面向对象编程最强大的特性之一。
15、虚函数 虚析构
虚函数:在基类中用virtual关键字修饰的成员函数。它的作用是允许在派生类中重写(override)它,从而实现动态多态。
动态绑定:当通过基类的指针或引用调用一个虚函数时,程序会在运行时根据指针实际指向的对象类型来决定调用哪个版本的函数,而不是根据指针本身的类型。
虚析构函数:如果你会通过基类指针来delete派生类对象,那么必须将基类的析构函数声明为虚函数。这样,delete时才会先调用派生类的析构函数,再调用基类的析构函数,避免内存泄漏。
cpp
class Animal {
public:
virtual ~Animal() { cout << "~Animal" << endl; }//虚析构
virtual void speak() { cout << "动物叫" << endl; }
};
class Dog : public Animal {
public:
virtual void speak() override { cout << "汪汪叫" << endl; }//重写虚函数
~Dog() { cout << "~Dog" << endl; }
};
int main() {
Animal* pet = new Dog();
pet->speak(); //输出:汪汪叫(动态绑定)
delete pet; //输出:~Dog -> ~Animal(因为虚析构)
return 0;
}
16、抽象类和纯虚构函数
纯虚函数:在虚函数声明的末尾上加上=0,它告诉编译器这个函数在当前类中没有实现。
抽象类:只要包含一个纯虚函数,这个类就成为抽象类。它不能被实例化(不能创建对象),只能作为基类来派生新类。其作用是为所有派生类提供一个统一的接口规范。
cpp
class Shape {//抽象类
public:
virtual double getArea() = 0;//纯虚函数,提供接口
};
class Circle : public Shape{
public:
Circle(double r) : radius(r) {}
//必须实现基类的纯虚函数,否则Circle也成为抽象类
virtual double getArea() override { return 3.14 * radius * radius; }
private:
double radius;
};
二、异常处理
1、概念
异常处理是C++用于管理程序运行时错误的核心机制,通过try、catch、throw等关键字实现。它允许将错误检测与处理逻辑分离,提升代码的可读性和健壮性。
核心优势:相比C语言通过返回错误码的方式,C++可以抛出一个对象,携带更全面的错误信息,并且可以沿着调用栈向上传播,无需层层返回错误码。
2、三个关键字
try:标记可能抛出异常的代码块
throw:抛出异常(可以是任何类型的值或对象)
catch:捕获并处理异常
3、基本语法
cpp
#include<iostream>
#include<vector>
using namespace std;
//可能抛出异常的函数
double divide(int a, int b) {
if (b == 0) {
throw runtime_error("除数不能为0!");//抛出异常对象
}
return (double)a / b;
}
int main() {
try {
cout << divide(10, 0) << endl;
}
catch (const runtime_error& e) {
cerr << "错误:" << e.what() << endl;
}
return 0;
}
4、异常抛出与匹配流程
4.1抛出与捕获流程
当throw执行时,throw后面的带不再执行
程序从throw位置跳到与之匹配的catch模块
抛出异常对象后,会生成一个异常对象的拷贝(因为抛出的可能是局部对象),该拷贝在catch子句结束后销毁
4.2栈展开
当异常被抛出后,程序会暂停当前函数的执行,沿着调用链逐层向上查找匹配的catch子句,这个过程称为栈展开。
下面代码关键点:被选中的catch是调用链中与抛出对象类型匹配且距离最近的那一个。
cpp
#include<iostream>
#include<vector>
using namespace std;
void func3() {
try {
throw 10;//抛出int类型异常
}
catch (float a) {//类型不匹配
cout << "func3" << endl;
}
}
void func2() {
try {
func3();
}
catch(int x){//匹配成功!
cout << "func2 捕获:" << x << endl;
}
}
void func1() {
try {
func2();
}
catch (int x) {
cout << "func1" << endl;
}
}
int main() {
func1();
return 0;
}
//输出:func2 捕获:10
4.3如果找不到匹配的catch
如果调用链中所有catch参数类型都与抛出对象不匹配,程序会调用std::terminate()终止程序。因此,通常会在main函数中写一个最终捕获:
cpp
try {
// 可能抛出异常的代码
} catch (...) { // 捕获任意类型的异常
cerr << "未知异常发生" << endl;
}
5、异常的重写抛出
有时候,在本函数中捕获异常后需要先做一些处理(如释放资源、记录日志),然后再将异常继续向上传递给外层调用链处理。
关键语法:throw;单独使用(不带参数)表示重新抛出当前捕获的异常。
cpp
void Func() {
int* array = new int[10];//动态分配资源
try {
int len, time;
cin >> len >> time;
if (time == 0) {
throw "Division by zero";
}
}
catch (...) {
cout << "释放资源:" << array << endl;
delete[] array; //释放资源
throw; //重新抛出,让上层处理
}
delete[] array;
}
int main() {
try {
Func();
}
catch (const char* errmsg) {
cout << errmsg << endl;
}
return 0;
}
6、自定义异常类
6.1通过继承std::exception,实际开发中,通常会通过继承std::exception来构建自己的异常体系。
cpp
//自定义异常基类
class MyException : public exception {
private:
string message;
public:
MyException(const char* msg) : message(msg) {}
const char* what() const noexcept override {
return message.c_str();
}
};
//派生异常类
class NetworkException : public MyException {
public:
NetworkException(const char* msg) : MyException(msg) {}
};
class ConnectionException : public NetworkException {
public:
ConnectionException(const char* msg) : NetworkException(msg) {}
};
//使用案例
void connectToServer() {
//模拟拦截失败
throw ConnectionException("无法连接到服务器");
}
int main() {
try {
connectToServer();
}
catch (const ConnectionException& e) {
cerr << "连接异常:" << e.what() << endl;
}
catch (const NetworkException& e) {
cerr << "网络异常:" << e.what() << endl;
}
return 0;
}
6.2优点:
提高代码的可读性和可维护性
不同的业务模块可以抛出特定的异常类型
通过异常类型即可区分不同的错误
7、标准库
cpp
std::exception
├── std::logic_error // 程序逻辑错误
│ ├── std::invalid_argument // 无效参数
│ ├── std::domain_error // 参数值域错误
│ ├── std::length_error // 长度超出限制
│ └── std::out_of_range // 超出有效范围
├── std::runtime_error // 运行时错误
│ ├── std::range_error // 结果超出值域
│ ├── std::overflow_error // 算术上溢
│ └── std::underflow_error // 算术下溢
├── std::bad_alloc // 内存分配失败
├── std::bad_cast // dynamic_cast失败
└── std::bad_typeid // typeid操作失败
三、模板
1、什么是模板
模板是C++实现泛型编程的核心机制,允许编写与类型无关的通用代码,让编译器根据实际使用情况自动生成对应类型代码。核心:编写一次,适用于多种数据类型,避免重复代码。
如:没有模板时,需要每种类型单独编写一个函数
cpp
void Swap(int& left, int& right) {
int temp = left;
left = right;
right = temp;
}
void Swap(double& left, double& right) {
double temp = left;
left = right;
right = temp;
}
使用模板后,一个函数适用于所有类型
cpp
template<typename T>
void Swap(T& left, T& right) {
T temp = left;
left = right;
right = temp;
}
2、函数模板
语法:
cpp
template<typename T1, typename T2, ..., typename Tn>
返回值类型 函数名(参数列表) {
// 函数体
}
int main(){
Add(1, 2);//隐式调用
Add<int>(1, 2.5);//显式调用
return 0;
}
//多类型模板参数
template<typename T1, typename T2>
auto Add(T1 a, T2 b) -> decltype(a + b) {
return a + b;
}
匹配规则:
1、非模板类型优先:如果存在完全匹配的普通函数,编译器会优先选择它
2、模板函数可以生成更匹配的版本:当模板能产生更适合的函数时,会选用模板
3、模板函数不允许自动类型转换,普通函数可以
3、类模板
实例:
cpp
template<typename T>
class Vector {
public:
Vector(size_t capacity = 10)
:_pData(new T[capacity])
, _size(0)
, _capacity(capacity)
{}
~Vector() {
delete[] _pData;
}
void PushBack(const T& data) {
_pData[_size++] = data;
}
T& operator[](size_t pos) {
return _pData[pos];
}
private:
T* _pData;
size_t _size;
size_t _capacity;
};
//类函数成员函数的类外定义
template<typename T>
Vector<T>::~Vector() {
if (_pData) delete[] _pData;
}
int main() {
Vector<int> intVec;//存储int类型
Vector<double> doubleVec;//存储double类型
return 0;
}
4、非类型模板函数
模板函数不仅可以是类型,还可以是编译期常量(整数、指针、引用等)。
cpp
template<typename T, size_t N>
class Array {
private:
T _data[N];
public:
size_t size() const { return N; }
};
int main() {
Array<int, 10> arr1; //10个整数的数组
Array<int, 100> arr2;//100个整数的数组
}
限制条件:
支持的类型:整数类型(int,size_t),指针、引用,C++20起支持浮点数。
不支持的类型:浮点数(double、float),类对象,字符串。
参数值必须是编译期常量,不能是运行时变量:
cpp
int n = 10;
// Array<int, n> arr; // ❌ 错误:n 不是编译期常量
Array<int, 10> arr; // ✅ 正确
5、模板特化
当通用模板对某些特殊类型不适用时,需要为这些类型提供专门实现,这就是模板特化。
全特化:所有模板参数都指定具体类型
cpp
//通用模板
template<typename T1, typename T2>
class Data {
public:
Data() { cout << "通用模板" << endl; }
};
//全特化
template<>
class Data<int, double> {
public:
Data() { cout << "全特化:int, double" << endl; }
};
偏特化:只限制部分参数或添加条件
cpp
//偏特化:第二个参数固定为 double
template<typename T>
class Data<T, double> {
public:
Data() { cout << "偏特化:T, double" << endl; }
};
//偏特化:两个参数都是指针类型
template<typename T1, typename T2>
class Data<T1*, T2*> {
public:
Data() { cout << "偏特化:指针,指针" << endl; }
};
四、STL
核心组件:容器
1、序列式容器
vector动态数组,尾部插入快,随机访问快,中间插入/删除慢,默认首选容器
deque双端队列,两端插入/删除快,随机访问较快,需要双端操作的场景
list双向链表,任意位置插入/删除快,不支持随机访问,频繁中间插入/删除
array静态数组,大小固定,性能接近原生数组,大小已知的固定集合
2、关联式容器
set元素唯一,自动升序,需要有序不重复集合
multiset允许重复元素,自动升序,需要有序可重复集合
map键值对,键唯一,按键排序,需要快速通过键查找值
multimap键可重复,一对多映射关系
3、非关联式容器
unordered_set元素唯一,无序,只需快速查找不关心顺序。
unordered_map键值对,键唯一,无序,快速键值查找,顺序无关。
还提供了算法(Algorithms)
排序:排序、归并,sort,stable_sort,partial_sort
查找:在序列中查找元素,find,binary_search,lower_bound
变序操作:反转、随机打乱,reverse,random_shuffle
删除:移除元素,remove,unique
排序:生成排列集合,next_permutation,prev_permutation
统计:计数、求和,count,accumulate
集合:并集、交集,set_union,set_intersection
迭代器(Iterators)
输入迭代器:只读,单向,istream_iterator
输出迭代器:只写,单向,ostream_iterator
前向迭代器:读写、单向,forward_list
双向迭代器:读写、双向,list,set,map
随机访问迭代器:读写、随机访问(支持算术运算),vector,deque,array
常用迭代器参数
begin()/end():获取首位迭代器
cbegin()/cend():获取常迭代器(只读)
rbegin()/rend():获取反向迭代器
advance(it, n):将迭代器前进n步
distance(first, last):计算两个迭代器间的距离
五、文件操作
1、概念
程序运行时的数据存储在内存中,一旦程序结束,这些数据就会丢失,文件操作可以解决这个问题:
数据持久化:将数据保存到硬盘中,程序结束后数据不丢失
数据交换:程序之间、系统之间可以通过文件传递数据
配置管理:读取配置文件,让程序行为可配置
日志记录:将程序运行信息写入日志文件,便于调试和追踪
2、文件操作的核心类
定义在<fstream>头文件中
ifstream:文件输入(从文件读取数据),ios::in
ofstream:文件夹输出(从文件写入数据),ios::out
fstream:文件输入输出(可读可写),ios::in | ios::out
ios::app:追加,写入的数据追加到文件末尾
ios::ate:尾部定位,打开文件后定位到文件末尾
ios::binary:二进制,以二进制模式打开,不进行内容转换
ios::trunc:截断,如果文件存在,清空文件内容
3、文本文件读写
4、文本模式和二进制模式的区别
5、文件指针定位
文本模式:会对数据进行转换处理,如Windows下换行符\n会被转换为\r\n
二进制模式(ios::binary):不对数据进行任何转换,原样存储