一、C到C++
1. C++完全兼容C
C++ == C + 面向对象 + C++标准库,C++可以直接调用C的所有库和C++编译器中使用C的语法。
C++:.cpp文件,编译:g++
C++是一门面向对象的语言,C语言则是面向过程的语言。
面向对象:以“对象”为核心,将数据和操作数据的方法封装在一起,通过类定义对象的属性(数据)和方法,通过实例化对象解决问题;支持封装、继承、多态三大特性。
面向过程:以“过程”为核心,将问题拆解成一系列步骤(函数),按顺序执行;数据和操作数据的函数分离,通过函数参数传递数据。
C语言:一切皆地址;C++:一切皆对象;Liunx:一切皆文件。
2. 函数的缺省(默认)参数
语法:
声明的时候写,定义的时候可以不写
写的默认值,只能从右往左
通过类型来区分
和函数重载一起使用可能会有二义性问题(编译器分不清到底用哪个)
3. 函数重载
函数名相同,参数列表不同,本质上是不同的函数。
参数列表不同:
(1)参数个数不同
(2)参数类型不同
(3)陈述顺序不同
(4)const int* 和 int *不同,一个是不可变的一个是可变的
以下情况并不是参数列表不同:
(1)函数返回值类型不同
(2)形参名不同
(3)const int 和 int类型没差别
为什么要使用函数重载?
一个功能(函数)作用于不同类型的数据,调用表达式可以不同改动。
4. 名字空间
什么是名字空间?
名字空间就是可以包含有众多名字(标识符)的空间。
一般是一个程序员在一个工程中的代码有一个名字空间;多个程序员完成一个工程。
为什么要用名字空间?
为了更好的区分;大规模团队协同作战。
5. 结构体和this指针
C++的结构体,联合,枚举在定义的时候必须使用关键字,在使用的时候可以省略关键字;c++的结构体可以有函数成员。
this指针就是指向结构体(对象)本身的指针;this指针是编译器负责管理,我们可以通过this关键字来(在成员函数中)访问对象。
6. 内存申请
C++可以使用C语言的动态内存申请方式,malloc、free等。
C++新增的动态申请方式:new delete new[] delete[]
二、类与对象
1. 类
类是一种用户自定义的数据类型,它定义了
对象的属性(数据成员) 和行为(成员
函数),相当于一个 “模板”,用于创建具体的对象。(默认是private)
class 类名 {
访问权限修饰符: // public(公有)、private(私有)、protected(保护)
数据成员; // 类的属性(变量)
成员函数; // 类的行为(函数)
};
2. 对象
对象是类的
实例化结果,即根据
类创建的具体个体。一个类可以创建多个对象,每个对象拥有独立的数据成员,但共享类的成员函数。
3. 构造函数与析构函数
构造函数
- 用于对象创建时初始化数据成员,与类名同名,无返回值,可重载(支持多种初始化方式)。
- 若未自定义,编译器会生成默认构造函数(无参)。
析构函数
- 用于对象销毁时释放资源(如动态内存),格式为~类名(),无参数,不可重载。
- 若未自定义,编译器会生成默认析构函数。
三、封装
1. 封装的目的
将成员变量和成员函数捆绑在一起,同时隐藏内部细节,只对外暴露必要的接口,增强代码的模块化与可维护性,提高安全性。
封装有三个关键字:pubic、private、protectd
public:声明下面的成员为公有成员(可以通过对象名来访问),直到遇到下一个封装关键字为止。
private:声明下面的成员为私有成员(不可以通过对象名来访问,只能通过类的公有成员函数来访问),直到遇到下一个封装关键字为止。
protected:声明下面的成员为保护成员(在类的内部可以直接访问,在派生类中也可以直接访问,但不能通过类的对象名在类外部访问),直到遇到下一个封装关键字为止。
2. struct和class的区别
(1)默认成员访问权限
struct成员的访问权限是public,class是private
(2)默认继承方式
当使用struct继承时,默认继承方式是public,而class是private
3. 封装其实是防君子不防小人
封装之后,声明对象,用一个
指针指向声明的对象,然后对指针进行操作,指向成员的所在位置,就可以改变其值。
#include <headcpp.h>
using namespace std;
class Student
{
private:
int id; // 4
string name; // 32
double score; // 8
int age; // 4
public:
void show()
{
cout << id << "--" << name << "--" << age << "岁--" << score << "分" << endl;
}
Student()
{
cout << "student类的无参构造" << endl;
}
Student(int id, string name, double score, int age)
{
this->id = id;
this->name = name;
this->score = score;
this->age = age;
cout << "student类的有参构造" << endl;
}
~Student()
{
cout << "student类的析构器" << endl;
}
};
int main(int argc, char *argv[])
{
Student *t = new Student(12, "刘星", 78.88, 19);
cout << "------------------------" << endl;
t->show();
cout << "------------------------" << endl;
int *p = (int *)t;
*p = 33;
t->show();
double *scorePtr = (double *)t;
scorePtr += 5; // 指向成绩所在的那片空间
*scorePtr = 98.99; // 修改值
t->show();
cout << "------------------------" << endl;
delete t;
cout << "------------------------" << endl;
return 0;
}
4. 内联函数
在类中声明和定义,或者在定义函数的时候用inline关键字修饰都是建议编译器设置函数为内联函数。
编译器到底有没有设置它为内联函数,我们不知道。
内联函数的作用
和宏函数类似,内联函数也是用膨胀内存的方式来节约函数调用的开销。
用宏函数或者内联函数:代码比较多,调用n次,就有n份代码,没有函数调用的跳转。
一般调用频率高并且代码量少的函数建议声明为内联函数或者使用宏函数。
如果函数中有循环或者递归,不建议使用内联函数或者宏函数。
内联函数和宏函数的区别
(1)宏函数在预处理的时候替换,内联函数在编译的时候替换;
(2)宏函数没有传参,内联函数有传参(运算完再传,而不是直接给);
(3)宏函数没有返回值,内联函数有返回值;
(4)宏函数替换之后再做语法检查,报错在调用的那一行;内联函数做完语法检查后再替换,报错在函数定义的那一行。
5. 接口
封装中的接口,指的是接口函数,让外界可以通过对象名和接口函数去获取或修改对象的属性,通常接口函数会被设置为内联函数。
四、拷贝构造
1. 理论
拷贝构造又称为复制构造,指的是用拷贝(复制)对象的方式来构造一个新对象。(不是自然生出来的,而是克隆出来的)
默认拷贝构造函数
如果用户没有显式定义拷贝构造函数,C++ 编译器会自动生成一个默认拷贝构造函数,其行为是逐个复制对象的所有成员变量(即 “
浅拷贝”)。
2. 自定义拷贝构造
为什么需要自定义拷贝构造函数(深拷贝)?
当类中包含指针成员或动态分配的资源(如堆内存)时,默认的浅拷贝会导致问题:
- 浅拷贝只会复制指针本身,而不会复制指针指向的内容,导致两个对象的指针指向同一块内存。
- 当对象销毁时,会对同一块内存释放两次,引发程序崩溃。
此时,需要自定义拷贝构造函数实现“深拷贝”,即复制指针指向的内容而非指针本身(重新分配一片新的内存,然后复制内容)
#include <headcpp.h>
using namespace std;
class myString
{
char *str;
int len;
public:
myString()
{
cout << "无参构造" << endl;
}
myString(const char *s = "")
{
cout << "有参构造" << endl;
len = strlen(s);
str = new char[len + 1];
strcpy(str, s);
}
myString(const myString &other)
{
cout << "拷贝构造" << endl;
len = other.len;
str = new char[len + 1]; // 深拷贝,需为其分配新的空间
strcpy(str, other.str); // 拷贝其内容,完成拷贝构造
strcpy(str, "hell0 kk!");
len = strlen(str);
}
~myString()
{
delete[] str;
cout << "析构函数" << endl;
}
void show()
{
cout << str << endl;
}
void getSize()
{
cout << len << endl;
}
};
int main(int argc, char *argv[])
{
cout << "-------------------" << endl;
myString s1("hello world!");
s1.show();
s1.getSize();
cout << "-------------------" << endl;
myString s2(s1);
s2.show();
s2.getSize();
cout << "-------------------" << endl;
return 0;
}
3. 引用
C语言中,函数传参有且仅有两种方式:
简单值传递和
地址传递。
简单值传递:单向,实参赋值给形参,函数内部无法改变实参;
地址传递:双向,函数内部可以改变实参所指向的内存段。
C++新增了一种类型:引用类型
如何定义一个引用: 类型名& 引用名 = 变量名;//
引用必须初始化
引用是内存段的别名,本身不占内存,必须初始化。
引用类型和基本数据类型是不同的类型,可以实现重载,但一起使用可能会有二义性问题。
引用和指针的区别?
(1)引用必须初始化,而指针不必;
(2)指针看有改指向,引用不能;
(3)指针可以改类型,引用不能;
(4)指针占内存,引用不占内存;
(5)有空指针,没有空引用;
(6)有多级指针,没有多级引用。
五、类的特殊成员
1. 类的常量成员
常量数据成员必须在
构造函数的初始化列表中进行初始化,不能在构造函数体内赋值。一旦初始化,其值在对象的生命周期内不可改变。
#include <iostream>
using namespace std;
class Circle {
private:
const double PI; // 常量数据成员
double radius;
public:
// 必须在初始化列表中初始化常量成员
Circle(double r) : PI(3.14159), radius(r) {} //自动传参列表
double getArea()
{
return PI * radius * radius;
}
};
int main() {
Circle c(5.0);
cout << "面积: " << c.getArea() << endl; // 输出: 面积: 78.5397
return 0;
}
在
成员函数的声明和定义后加上 const,表示该函数不会修改类的任何数据成员,也不能调用非 const 成员函数。
class Rectangle {
private:
int length;
int width;
public:
Rectangle(int l, int w) : length(l), width(w) {}
// 常量成员函数(不会修改数据成员)
int getArea() const {
// length = 10; // 错误:不能修改成员变量
return length * width;
}
// 非const成员函数(可以修改数据成员)
void setLength(int l) {
length = l;
}
};
int main() {
const Rectangle r(3, 4); // 常量对象
cout << "面积: " << r.getArea() << endl; // 正确:可以调用const成员函数
// r.setLength(5); // 错误:常量对象不能调用非const成员函数
return 0;
}
2. 类的静态成员
static修饰类的成员变量
- 可以通过类名直接访问(类名::成员名),也可通过对象访问。
- 类的静态成员变量属于类不属于对象。
- 类的所有对象共享同一个静态成员变量。
- 类的静态成员变量必须在类外初始化。
static修饰类的成员函数
- 只能访问静态数据成员和其他静态成员函数,不能访问非静态成员。
- 可以通过类名直接调用(类名::函数名()),也可通过对象调用。
#include <iostream>
using namespace std;
class Student {
private:
string name;
static int totalCount; // 静态数据成员(类内声明)
public:
Student(string n) : name(n) {
totalCount++; // 每创建一个对象,总数加1
}
void showName() {
cout << "姓名: " << name << endl;
}
static int getTotalCount() { // 静态成员函数
return totalCount;
}
};
int Student::totalCount = 0; // 静态数据成员必须在类外初始化(分配内存)
int main() {
Student s1("张三");
Student s2("李四");
// 通过类名访问静态成员函数,获取总人数
cout << "学生总数: " << Student::getTotalCount() << endl; // 输出:2
// 通过对象也能访问静态成员(不推荐,语义不清晰)
cout << "学生总数: " << s1.getTotalCount() << endl; // 输出:2
return 0;
}
六、运算符重载
1. 理论
运算符:
本质是函数,是编译器提前写好的,具备特殊功能的符号。
所以运算符重载本质上是函数重载
表达式:
由值、关键字、运算符、标识符组成的,具备唯一返回值的式子。
2. 如何使用
运算符重载的返回值类型名
运算符表达式 返回值
没有返回值就是 void
有返回值就是返回值的类型
运算符重载函数名
operator 运算符
例如:
operator +
operator-
operator new
operator delete[]
运算符重载函数参数列表
运算符分:单目运算符 双目运算符 三目运算符
目:运算符操作数的个数
操作数:运算符可以控制的表达式
左值:可以作为赋值运算符的左操作数的表达式叫做左值,可写访问
右值:只能作为赋值运算符的右操作数的表达式 ,不可写访问,只读
所以,运算符重载函数要么有两个参数,要么有一个参数,要么没有参数。
如果运算符重载函数声明为类的
成员函数:当前对象本身就是一个操作数,双目运算符重载函数只有一个参数,单目运算符重载函数没参数。
如果运算符重载函数声明为类的
友元函数:类的友元函数不是类的成员函数,双目运算符重载函数有两个参数,单目运算符重载函数有一个参数。
运算符重载函数函数体
自己写,但是一般不要改变运算符的功能
运算符的优先级,执行顺序,运算符的目数等都不要改变。
注意事项:
(1)不要创造新的运算符,不要改变运算符的功能,不要改变运算符的优先级,结合顺序等
(2)一般双目运算符选择重载为友元,单目运算符一般重载为成员。有特殊规定除外(赋值运算符只能重载为成员;输入输出运算符只能重载为友元。)
3. 友元
(1)什么是友元
一个类A可以认定某个函数f或者某个类B是它的朋友。认定之后,函数f或者B类的对象可以访问类A的私有成员。这就叫友元,或者叫友缘。
函数f称之为类A的友元函数。类B称之为类A的友元类。类B的所有成员函数都是类A的友元函数。
友元认定是单向认定。比如A认定B是它的朋友,B未必认定A是它的朋友。也就是说A认定B是A的朋友之后,B类的对象可以访问A类对象的私有成员但A类对象未必可以访问B类对象的私有成员。
(2)声明
类A要声明某个函数为它的
友元函数,需要在类中声明函数并且前面加friend关键字
friend 函数声明语句;
friend int f(int,double);
类A要声明某个类为它的
友元类,需要在类中声明类并且前面加friend关键字
friend class 类名;
friend class B;
#include <headcpp.h>
using namespace std;
class myArray
{
int *arr;
int len;
friend class math; // 友元类
friend class B;
friend void sortA(myArray &obj, bool type); // 友元函数
public:
myArray(int *arr, int len) : arr(arr), len(len) {};
void show()
{
for (int i = 0; i < len; i++)
{
cout << arr[i] << " ";
}
cout << endl;
}
};
class math
{
public:
static void sort(myArray &obj, bool type = true)
{
for (int i = 0; i < obj.len - 1; i++)
{
for (int j = 0; j < obj.len - i - 1; j++)
{
if (type ? (obj.arr[j] > obj.arr[j + 1]) : (obj.arr[j] < obj.arr[j + 1]))
{
int temp = obj.arr[j];
obj.arr[j] = obj.arr[j + 1];
obj.arr[j + 1] = temp;
}
}
}
}
};
void sortA(myArray &obj, bool type = false)
{
for (int i = 0; i < obj.len - 1; i++)
{
for (int j = 0; j < obj.len - i - 1; j++)
{
if (type ? (obj.arr[j] > obj.arr[j + 1]) : (obj.arr[j] < obj.arr[j + 1]))
{
int temp = obj.arr[j];
obj.arr[j] = obj.arr[j + 1];
obj.arr[j + 1] = temp;
}
}
}
}
class B
{
public:
static void showB(myArray &obj, bool type = false)
{
if (type == false)
{
for (int i = 0; i < obj.len; i++)
{
cout << obj.arr[i] << " ";
}
cout << endl;
}
if (type == true)
{
for (int i = obj.len - 1; i >= 0; i--)
{
cout << obj.arr[i] << " ";
}
cout << endl;
}
}
};
int main(int argc, char *argv[])
{
cout << "------------------------------------------------------" << endl;
int arr[6] = {54, 66, 33, 36, 86, 45};
myArray myA(arr, 6);
math::sort(myA);
cout << "友元函数->math类成员函数排序: ";
myA.show();
cout << "------------------------------------------------------" << endl;
sortA(myA);
cout << "友元函数->单独函数设友元排序: ";
myA.show();
cout << "------------------------------------------------------" << endl;
int arr2[6] = {54, 66, 33, 36, 86, 45};
myArray myB(arr2, 6);
B::showB(myB);
B::showB(myB, true);
return 0;
}
(3)友元函数使用缺省参数列表➡️声明为友元的同时声明函数
(4)内部类
#include <headcpp.h>
using namespace std;
class myList
{
class Node
{
int data; // 数据域
Node *pNext; // 指针域
public:
Node()
{
data = 0;
pNext = NULL;
}
Node(int d)
{
data = d;
pNext = NULL;
}
void setData(int d)
{
data = d;
}
int getData(void) const
{
return data;
}
void setNext(Node *p)
{
pNext = p;
}
Node *getNext(void) const
{
return pNext;
}
};
Node *pList; // 指向链表头的指针
public:
myList()
{
pList = NULL;
}
void append(int appendData) // 尾插
{
if (pList != NULL)
{
// 找到链表末尾
Node *p = pList;
while (p->getNext())
{
p = p->getNext();
}
// 给末尾添加一个结点
Node *pNew = new Node(appendData);
p->setNext(pNew);
}
else
{
pList = new Node(appendData);
}
}
void add_Head(int data)
{
Node *pNew = new Node(data);
pNew->setNext(pList); // 新节点的next指向原来的头节点
pList = pNew; // 头指针指向新节点,使新节点成为新的头节点
}
void show()
{
cout << "list:";
Node *p = pList;
while (p != NULL)
{
cout << p->getData() << " ";
p = p->getNext();
}
cout << endl;
}
};
int main(int argc, char *argv[])
{
myList l;
for (int i = 1; i <= 10; i++)
{
l.append(i);
}
l.show();
l.add_Head(18);
l.show();
return 0;
}
4. 输入输出运算符重载
输出
函数名: operator<<
左操作数: cout ostream
右操作数: 当前类对象的引用 用const修饰
返回值:ostream&
cout << n << a
输入
函数名:
operator>>
返回类型名:
istream& 可以连用 说明 >> 返回值是 istream&
参数列表:
左操作数: istream&
右操作数: 当前类对象的引用 不要加const
七、继承
1. 理论
一个类A继承自某个类B,A类叫做子类或者
派生类,B类叫做父类或者
基类。
那么A类的所有对象拥有B类的成员变量和成员函数。
2. 作用
(1)
代码复用
子类可直接继承父类的属性和方法,无需重复编写共性代码,减少冗余。
(2)构建层次关系
通过 "父类 - 子类" 结构,体现现实世界的层级逻辑(如 "动物 - 狗 - 哈士奇"),使类关系更清晰。
(3)支撑多态特性
继承是多态的基础。基于继承,可通过父类指针/引用调用子类重写的虚函数,实现运行时的动态绑定,提升代码灵活性。
(4)便于扩展维护
修改父类的共性逻辑时,所有子类自动受益;新增功能时只需派生新类,不影响原有代码。
3. 简单继承
在定义派生类的时候继承基类,注意不能循环继承。
class 派生类类名:继承方式 基类类名
{
};
#include <headcpp.h>
using namespace std;
class horse
{
public:
string name;
string type;
int age;
string sex;
bool isQi;
horse(string name, string type, int age, string sex, bool Qi) : name(name), type(type), age(age), sex(sex), isQi(Qi) {}
void show()
{
cout << "这是一匹叫" << name << "的" << type << ",是一个" << sex << "哦,今年" << age << "岁了" << endl;
}
void Qi()
{
if (isQi)
{
cout << name << "被骑着出去玩儿了" << endl;
}
else
{
cout << name << "正在马厩里休息" << endl;
}
}
};
class white_Horse : public horse
{
public:
// 子类构造函数,通过初始化列表调用父类构造函数
white_Horse(string name, string type, int age, string sex, bool Qi) : horse(name, type, age, sex, Qi) {}
void shuaS()
{
cout << name << "正在耍帅!不务正业" << endl;
}
};
int main(int argc, char *argv[])
{
horse h1("多莉", "汗血宝马", 8, "妹妹", true);
h1.show();
h1.Qi();
cout << "----------------------------------------------" << endl;
white_Horse wh1("小白", "白马", 6, "弟弟", true);
wh1.show();
wh1.Qi();
wh1.shuaS();
return 0;
}
4. 继承过程中的封装
public : 公有,可以访问的
private:私有,不能访问的,只有当前类对象成员可以访问
protected:保护的,介于public和private之间;派生类对象可以访问,其他类对象不能访问;如果是派生类对象,看作public,如果是其他类对象,看作private
5. 复杂继承
一个类既可以继承自多个类
一个类也可以被多个类继承
菱形继承
由于 B 和 C 都单独继承 A,子类 D 继承 B 和 C 时,会
间接拥有两份 A 的成员(一份来自 B→A,一份来自 C→A),导致两个关键问题:
- 数据冗余:D 的对象中会存储两份 A 的成员(如x存了两次),浪费内存;
- 访问歧义:直接访问 A 的成员时,编译器无法判断该用 “B 继承的 A” 还是 “C 继承的 A”,直接报错。
6. 虚继承
C++ 通过
虚继承
解决菱形继承的问题:在 “B 继承 A” 和 “C 继承 A” 时,在继承方式前加
virtual
关键字,强制让 B 和 C “共享一份 A 的成员”,确保 D 的对象中只保留
一份 A 的成员
,既消除冗余,又解决歧义。
class A {
public:
int x;
};
// 关键:B和C用“虚继承”方式继承A
class B : virtual public A {};
class C : virtual public A {};
// D继承B和C,此时A的成员只有一份
class D : public B, public C {};
int main() {
D d;
d.x = 10; // 正确!无歧义,只有一份x
return 0;
}
虚继承的本质是:让 “共享的顶层基类(A)” 成为 “虚基类”,子类(B、C)不再单独存储 A 的成员,而是通过一个 “间接指针” 指向同一份 A 的成员,最终让 D 只拥有一份 A 的实例。
7. 虚析构
为什么需要虚析构函数?
当用
基类指针指向派生类对象
,且派生类中存在
动态内存(如 new 分配的指针)
时,若基类析构函数不是虚函数,删除基类指针时
只会调用基类析构函数,不会调用派生类析构函数
,导致派生类中的动态内存无法释放,造成内存泄漏。
- 虚析构函数会触发 “动态绑定”:编译器根据指针实际指向的对象类型(而非指针声明类型)调用对应的析构函数。
#include <iostream>
using namespace std;
class A {
public:
A() { cout << "A construct" << endl; }
virtual ~A() { cout << "A deconstruct" << endl; } // 虚析构
};
class B : public A {
public:
int* p;
B() {
p = new int;
cout << "B construct" << endl;
}
~B() {
delete p;
cout << "B deconstruct" << endl;
}
};
int main() {
A* p = new B;
delete p; // 先调用B::~B(),再调用A::~A()
return 0;
}
// 输出:
// A construct
// B construct
// B deconstruct (派生类析构执行,释放p)
// A deconstruct (基类析构执行)
8. 覆盖
如果基类有某个函数show,派生类也定义了一个一样的函数show。
如果show函数原型一致,不构成重载。就会产生覆盖。
用基类指针指向派生类对象,调用基类成员函数;用派生类指针指向派生类对象,调用派生类成员函数(覆盖)。
#include <headcpp.h>
using namespace std;
class A
{
public:
int n;
public:
A() { cout << "A construct" << endl; }
~A() { cout << "A deconstruct" << endl; }
A(int n) : n(n) {}
void show()
{
cout << "这是基类A的show--n:" << n << endl;
}
};
class B : public A
{
public:
int n;
public:
B() { cout << "B construct" << endl; }
~B() { cout << "B deconstruct" << endl; }
B(int a) : n(a) {}
B(int a, int b) : n(a), A(b) {}
void show()
{
cout << "这是派生类B的show--n:" << n << endl;
}
};
int main()
{
A *pa = new B(1, 2); // B::n == 1 B::A::n == 2
B *pb = new B(3);
cout << pb->n << endl; // 3
cout << pa->n << endl; // 2 基类指针指向派生类对象中的基类对象
B *pc = new B(5, 6);
A *pd = pc;
pc->show(); // 派生类继承自基类的成员函数,如果派生类中写了一个一样的,覆盖掉基类的
pd->show(); // 基类指针指向派生类对象,调用基类show
return 0;
}
八、多态
1. 理论
多态是指同一接口(如函数名)在不同对象上表现出不同行为的特性,主要分为两种:
静态多态(编译时多态):
通过函数重载(同一作用域内函数名相同但参数不同)或模板实现,编译时就确定具体调用哪个版本。
动态多态(运行时多态):
基于继承和虚函数,
子类重写父类的虚函数,通过父类指针 / 引用指向子类对象时,运行时自动调用子类的实现。
2. 重写
重写指的是子类对父类中虚函数的重新实现,但需满足如下条件:
(1)
父类中的函数必须声明为
virtual
(虚函数)
(2)父类虚函数和子类重写后的
函数原型一致(函数名,参数列表一致,返回类型名不会检查但要保持一致)
如果父类函数修饰为虚函数并且子类重写这个函数。父类指针指向子类对象,会调用子类成员函数而不是父类成员函数。
class Animal {
public:
virtual void makeSound() { /* 父类虚函数 */ }
};
class Dog : public Animal {
public:
void makeSound() override { /* 重写父类虚函数,实现狗叫 */ }
};
3. 虚函数表
在 C++ 中,虚函数表是编译器实现动态多态的核心机制,本质是一个存储类中所有
虚函数地址的数组(或函数指针表)。
当一个类包含虚函数(或继承了虚函数)时,编译器会为该类自动生成一个虚函数表,表中按顺序存放该类所有虚函数的入口地址。同时,类的每个对象会隐含一个指向该虚函数表的指针(称为虚指针 vptr),占 4 字节(32 位系统)或 8 字节(64 位系统)内存。
虚函数表的作用:
当通过父类指针 / 引用调用虚函数时,程序会:
(1)通过对象的 vptr 找到所属类的虚函数表;
(2)在虚函数表中找到对应虚函数的地址;
(3)调用该地址指向的函数(可能是父类的实现,也可能是子类重写的实现)。
这个过程在运行时完成,实现了 “动态绑定”(即同一调用语句在不同对象上表现出不同行为)。
简言之,虚函数表是 C++ 实现动态多态的底层机制,通过存储虚函数地址和对象的 vptr 指针,实现了运行时对函数版本的动态选择。
4. 抽象类与接口
纯虚函数
如果一个虚函数没有函数体,那么这个函数称为纯虚函数。
纯虚函数只有声明,没有定义。
virtual 返回类型名 函数名(参数列表) = 0;
抽象类
如果一个类中有纯虚函数,这个类叫做抽象类。
抽象类没有对象。
抽象类必须被继承(不继承不会报错,但没意义)。
抽象类中所有纯虚函数必须被重写,不重写派生类也是抽象类。
#include <headcpp.h>
using namespace std;
class A
{
protected:
int n;
public:
A(int n) : n(n) {}
virtual void show(const char *str) = 0; // 虚函数
virtual void haha() = 0; // 虚函数
};
class B : public A
{
public:
B(int n) : A(n) {}
void show(const char *str)
{
cout << "show " << n << ",str:" << str << endl;
}
void haha()
{
}
};
int main(int argc, char *argv[])
{
B b(222);
b.show("hello lm");
return 0;
}
接口
接口,是一种特殊的类结构,核心作用是定义 “行为规范”—— 只声明必须实现的函数(方法),不提供具体实现,强制子类遵循统一的行为标准,本质是 “纯虚函数的集合”。
关键特征:
(1)
仅含纯虚函数:类中所有成员函数都声明为
virtual 返回类型 函数名(参数) = 0;(
=0 表示纯虚函数,无函数体);
(2)
无普通成员变量:接口只定义 “能做什么”,不存储数据(若有成员,通常是静态常量);
(3)
子类必须实现所有纯虚函数:子类继承接口后,必须重写接口中的所有纯虚函数,否则子类也会变成 “抽象类”(无法实例化)。
#include <iostream>
#include <cmath> // 用于圆形面积计算(M_PI)
// 1. 定义接口:图形接口(抽象基类)
class Shape {
public:
// 纯虚函数:计算面积(子类必须实现)
virtual double calculateArea() const = 0;
// 纯虚函数:计算周长(子类必须实现)
virtual double calculatePerimeter() const = 0;
// 虚析构函数:确保删除子类对象时调用正确的析构函数
virtual ~Shape() {}
};
// 2. 实现接口:圆形类(继承并实现Shape接口)
class Circle : public Shape {
private:
double radius; // 圆形的成员变量(接口不存储,子类自行定义)
public:
// 构造函数:初始化半径
Circle(double r) : radius(r) {}
// 重写接口的纯虚函数:计算圆形面积
double calculateArea() const override {
return M_PI * radius * radius; // M_PI是cmath中的圆周率常量
}
// 重写接口的纯虚函数:计算圆形周长
double calculatePerimeter() const override {
return 2 * M_PI * radius;
}
// 子类可额外添加自己的方法(接口不限制)
double getRadius() const {
return radius;
}
};
// 2. 实现接口:矩形类(继承并实现Shape接口)
class Rectangle : public Shape {
private:
double width;
double height;
public:
Rectangle(double w, double h) : width(w), height(h) {}
// 重写接口函数:计算矩形面积
double calculateArea() const override {
return width * height;
}
// 重写接口函数:计算矩形周长
double calculatePerimeter() const override {
return 2 * (width + height);
}
};
// 3. 使用接口:通过接口指针调用子类实现(多态)
void printShapeInfo(const Shape* shape) {
if (shape == nullptr) return;
// 调用接口函数,自动触发子类的具体实现
std::cout << "面积:" << shape->calculateArea() << std::endl;
std::cout << "周长:" << shape->calculatePerimeter() << std::endl;
std::cout << "------------------------" << std::endl;
}
int main() {
// 不能直接实例化接口(抽象类),必须通过子类对象
Shape* circle = new Circle(5.0); // 接口指针指向圆形对象
Shape* rectangle = new Rectangle(3.0, 4.0); // 接口指针指向矩形对象
// 多态调用:同一接口,不同实现
std::cout << "圆形信息:" << std::endl;
printShapeInfo(circle);
std::cout << "矩形信息:" << std::endl;
printShapeInfo(rectangle);
// 释放内存(虚析构确保子类析构被调用)
delete circle;
delete rectangle;
return 0;
}
九、泛型与模板
1. 理论
泛型
编写与具体数据类型无关的通用代码,适配多种类型
模板
实现泛型的语法工具,模板是泛型的 “实现方式”,泛型是模板的 “设计目标”。
2. 函数模板
用template (T是 “类型参数”,可自定义名称)声明模板,函数中用T代替具体类型,编译器会根据调用时的实参类型,自动生成对应版本的函数(称为 “模板实例化”)。
泛型参数列表中有非泛型
#include <headcpp.h>
using namespace std;
template <typename T, size_t len> // 定义一个模板函数function,有两个参数: 类型T和长度len
T function(T *a, bool isAfter) // 函数接收T类型和指针和bool类型isAfter,返回T类型值
{
if (isAfter)
cout << "after : ";
else
cout << "before: ";
for (size_t i = 0; i < len; i++)
cout << a[i] << ", ";
cout << endl;
return a[0];
}
int main(int argc, char *argv[])
{
int a[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
double b[10] = {1.1, 2.2, 3.3, 4.4, 5.5, 6.6, 7.7, 8.8, 9.9, 11.11};
int ret1 = function<int, 10>(a, true);
double ret2 = function<double, 10>(b, false);
return 0;
}
泛型参数列表中只有泛型参数
#include <iostream>
#include <string>
using namespace std;
template <typename T>
void function(T *a, int len, bool isAfter = false)
{
if (isAfter)
cout << "after :";
else
cout << "before:";
for (int i = 0; i < len; i++)
cout << a[i] << " ";
cout << endl;
}
int main()
{
int a[10] = {1, 2, 3, 4, 5, 6, 7, 8};
double b[10] = {1.2, 2.2};
return 0;
}
3. 类模板
用
template
声明类模板,类中成员变量、成员函数的类型可通过
T
指定,实例化时需显式指定类型(如
vector
)。类模板必须显式调用。
// 类模板:T是数组存储的元素类型
template <typename T>
class MyArray {
private:
T* data; // 存储T类型的数组
int size;
public:
// 构造函数:初始化size个T类型元素
MyArray(int s) : size(s) {
data = new T[size];
}
// 取值:返回T类型元素
T& get(int index) {
return data[index];
}
~MyArray() { delete[] data; }
};
int main() {
MyArray<int> intArr(5); // 实例化:T=int,存储int的数组
intArr.get(0) = 10; // 赋值int类型
MyArray<double> doubleArr(3); // 实例化:T=double,存储double的数组
doubleArr.get(1) = 3.14; // 赋值double类型
return 0;
}
4. typename和class的区别及类模板注意事项
类模板的文件存放限制
类模板
不能单独编写在.cpp 文件中
,必须将其
声明和实现
都放在.h头文件中。这是因为类模板在编译阶段需要根据具体的模板参数(如
int
、
Student
)实例化出具体的类,若将实现放在.cpp 文件,编译器在处理调用代码时无法找到模板的实现细节,会导致链接错误。
typename 的特殊用途:修饰类模板的内部类类型
当函数返回值为
类模板的内部类类型
时,必须用
typename
修饰该内部类类型,不能用
class
。
- 原因:编译器在解析模板代码时,无法直接判断MyList::Node是 “类型” 还是 “成员变量”,typename的作用就是明确告知编译器,其后的标识符是一个类型。
MyList.h
#pragma once
#include <iostream>
#include <string>
using namespace std;
// 类模板MyList,实现链表功能
template<class T>
class MyList {
// 内部类:链表节点(存储数据和下一个节点指针)
struct Node {
T data; // 节点存储的数据
Node* pNext; // 指向后一个节点的指针
// 节点构造函数
Node(const T& d) : data(d), pNext(nullptr) {}
~Node() {} // 析构函数(无动态内存,空实现)
};
Node* pHead; // 链表头指针(指向第一个节点)
public:
MyList(); // 链表构造函数(初始化头指针)
~MyList(); // 链表析构函数(释放所有节点)
void clear(); // 清空链表(释放所有节点,头指针置空)
void push_front(const T& data); // 头插法添加节点
void travel(); // 遍历链表并打印数据
Node* getHead(); // 获取头节点(返回内部类Node类型)
private:
void _clear(); // 私有辅助函数:实际执行节点释放逻辑
};
// 关键:返回内部类Node*,必须用typename修饰MyList<T>::Node
template<class T>
typename MyList<T>::Node* MyList<T>::getHead() {
return pHead;
}
// 头插法实现:新节点插入到链表头部
template<class T>
void MyList<T>::push_front(const T& data) {
Node* pNew = new Node(data); // 创建新节点
pNew->pNext = pHead; // 新节点的下一个节点指向原头节点
pHead = pNew; // 头指针更新为新节点
}
// 遍历链表实现
template<class T>
void MyList<T>::travel() {
Node* p = pHead;
cout << "list:";
while (p != nullptr) {
cout << p->data << " "; // 打印当前节点数据
p = p->pNext; // 移动到下一个节点
}
cout << endl;
}
// 清空链表的公有接口(调用私有辅助函数)
template<class T>
void MyList<T>::clear() {
_clear();
}
// 私有辅助函数:释放所有节点
template<class T>
void MyList<T>::_clear() {
Node* pCur = pHead;
while (pCur != nullptr) {
Node* pTemp = pCur; // 临时保存当前节点
pCur = pCur->pNext; // 移动到下一个节点
delete pTemp; // 释放当前节点
}
pHead = nullptr; // 头指针置空(避免野指针)
}
// 链表构造函数:初始化头指针为nullptr(空链表)
template<class T>
MyList<T>::MyList() : pHead(nullptr) {}
// 链表析构函数:调用_clear释放所有节点
template<class T>
MyList<T>::~MyList() {
_clear();
}
main.cpp
#include "MyList.h"
#include <iostream>
using namespace std;
// 自定义结构体Student(用于测试链表存储复杂类型)
struct Student {
char name[20]; // 姓名(用char数组避免string的额外内存开销)
int age; // 年龄
double score; // 分数
// 友元函数:重载<<,支持cout直接打印Student对象
friend ostream& operator<<(ostream& o, const Student& s) {
return o << s.name << "," << s.age << "," << s.score;
}
};
int main() {
// 1. 测试存储int类型的链表
int arr[10] = {1,2,3,4,5,6,7,8,9,0};
MyList<int> lInt;
for (int i = 0; i < 10; i++) {
lInt.push_front(arr[i]); // 头插法添加int数据
}
lInt.travel(); // 输出:list:0 9 8 7 6 5 4 3 2 1
// 2. 测试存储Student类型的链表
Student s[5] = {
{"曹操",30,33.33},
{"刘备",35,37.33},
{"孙权",20,22.33},
{"汉献帝",10,10.33},
{"司马昭",5,99.99}
};
MyList<Student> lStu;
for (int i = 0; i < 5; i++) {
lStu.push_front(s[i]); // 头插法添加Student数据
}
lStu.travel(); // 输出:list:司马昭,5,99.99 汉献帝,10,10.33 孙权,20,22.33 刘备,35,37.33 曹操,30,33.33
// 3. 测试getHead():获取头节点并打印数据
cout << lStu.getHead()->data << endl; // 输出:司马昭,5,99.99
cout << lInt.getHead()->data << endl; // 输出:0
return 0;
}
十、STL 标准模板库
1. 核心组成
STL 是 C++ 标准库的核心,包含三大组件,组件间可灵活搭配使用:

2. 容器
通用特点
- 均为类模板:支持存储任意类型(如vector、vector)。
- 头文件与类名一致:如#include 对应vector容器。
- 所有组件均在std命名空间下:使用时需加using namespace std;或std::vector。
序列式容器(按插入顺序存储,元素有明确位置)
(1)
vector(动态数组)
- 特点:连续内存存储,支持随机访问(通过下标快速访问元素),尾部插入 / 删除效率高(O (1)),中间插入 / 删除效率低(需移动元素,O (n)),内存会自动扩容(通常扩容为原大小的 1.5~2 倍)。
- 适用场景:需要频繁随机访问、尾部增删,且元素数量不确定的场景(如存储列表数据、临时计算结果)。
#include <vector>
#include <iostream>
using namespace std;
int main() {
vector<int> vec; // 初始化空vector
vec.push_back(10); // 尾部插入:[10]
vec.push_back(20); // [10,20]
vec.insert(vec.begin() + 1, 15); // 中间插入:[10,15,20]
// 遍历方式1:下标访问(随机访问)
for (int i = 0; i < vec.size(); ++i) {
cout << vec[i] << " "; // 输出:10 15 20
}
// 遍历方式2:迭代器
for (vector<int>::iterator it = vec.begin(); it != vec.end(); ++it) {
cout << *it << " ";
}
vec.pop_back(); // 尾部删除:[10,15]
cout << "\n大小:" << vec.size() << ",容量:" << vec.capacity() << endl; // 大小2,容量可能为4(扩容后)
vec.clear(); // 清空元素(大小变0,容量不变)
return 0;
}
(2)
list(双向链表)
- 特点:非连续内存存储(节点通过指针连接),不支持随机访问(访问元素需从头 / 尾遍历,O (n)),任意位置插入 / 删除效率高(仅需修改指针,O (1)),额外存储指针开销较大。
- 核心优势:中间插入 / 删除无需移动元素,适合频繁调整元素位置的场景。
#include <list>
#include <iostream>
using namespace std;
int main() {
list<int> lst = {1, 2, 3};
lst.push_front(0); // 头部插入:[0,1,2,3]
lst.insert(++lst.begin(), 0.5); // 第二个位置插入:[0,0.5,1,2,3]
lst.remove(0); // 删除所有值为0的元素:[0.5,1,2,3]
// 遍历(仅支持迭代器/范围for,无下标)
for (auto val : lst) {
cout << val << " "; // 输出:0.5 1 2 3
}
return 0;
}
(3)
deque(双端队列)
- 特点:分段连续内存(用数组存储分段指针),支持两端快速增删(O (1)),也支持随机访问(O (1),但效率略低于 vector),中间插入 / 删除效率低(O (n))。
- 适用场景:需要在头部和尾部频繁操作的场景(如实现队列、滑动窗口)。
- 关键操作:push_front()(头部插入)、push_back()(尾部插入)、pop_front()(头部删除)、pop_back()(尾部删除)。
关联式容器(按键值有序存储,自动排序,基于红黑树实现)
(1)
map(键值对映射,键唯一)
- 特点:存储 键值对,键唯一且自动按升序排序(默认 less),通过键查找效率高(O (log n)),支持通过键访问值。
#include <map>
#include <iostream>
using namespace std;
int main() {
// 初始化:键(姓名)唯一,自动按姓名首字母排序
map<string, int> score = {{"Alice", 95}, {"Bob", 88}};
score["Charlie"] = 92; // 插入键值对:{"Charlie":92}
// 查找键(存在则返回迭代器,不存在返回end())
auto it = score.find("Bob");
if (it != score.end()) {
cout << "Bob's score: " << it->second << endl; // 输出:88
}
// 遍历(按键升序)
for (auto& pair : score) {
cout << pair.first << ": " << pair.second << endl;
// 输出:Alice:95 → Bob:88 → Charlie:92
}
return 0;
}
(2)
set(存储唯一元素,自动排序)
- 特点:仅存储单个元素(无值),元素唯一且自动升序排序,查找 / 插入 / 删除效率均为 O (log n),本质是 “无值的 map”。
- 适用场景:需要去重并排序的场景(如存储不重复的 ID、统计唯一元素)。
- 关键操作:insert()(插入,重复元素会被忽略)、count(x)(判断 x 是否存在,返回 0 或 1)、erase(x)(删除元素 x)。
(3)
multimap /
multiset(键 / 元素可重复)
- 特点:与 map/set 逻辑一致,但键 / 元素可重复,排序规则相同,查找时需处理多个匹配结果(如用 equal_range(x) 获取所有值为 x 的元素范围)。
- 适用场景:需要存储重复键 / 元素的场景(如 multimap 存储 “同一班级的多个学生”,键为班级名,值为学生信息)。
3. 迭代器
在 C++ 中,
迭代器(Iterator)
是连接容器与算法的 “桥梁”—— 它提供了一种统一的方式访问容器中的元素,无论容器底层是数组(如
vector
)、链表(如
list
)还是哈希表(如
unordered_map
),都能通过迭代器遍历、修改元素,无需关心容器的内部实现细节。
作用
(1)统一访问接口:无论容器类型如何,迭代器都提供
++(移动到下一个元素)、
*(获取当前元素)等统一操作,让遍历逻辑与容器类型解耦。
(2)适配算法:STL 算法(如
sort、
find)依赖迭代器工作,例如
sort(iter1, iter2) 可对任意容器的
[iter1, iter2) 区间排序。
(3)安全访问:避免直接操作容器底层数据(如数组下标越界),部分迭代器(如
vector 的随机访问迭代器)还支持范围检查。
逾尾迭代器(end ())
vector的
end()返回的不是最后一个元素的迭代器,而是
最后一个元素的下一个位置(“逾尾” 位置),仅用于判断遍历结束,不能解引用(
*it会报错)。
- 遍历正确写法:for (it = v.begin(); it != v.end(); it++)(用!=而非<,适配所有容器)。
迭代器失效
迭代器本质是 “指向容器元素内存地址的指针”,当容器发生以下操作时,元素的内存地址可能变化,导致原有迭代器失效(继续使用会触发段错误或脏数据):
- 扩容(push_back时空间不足,重新开辟内存并拷贝数据);
- 删除元素(erase)或插入元素(insert)。
- 每次容器操作后,重新初始化迭代器(如it = v.begin())。
#include <vector>
#include <iostream>
using namespace std;
int main() {
vector<int> v = {0,1,2,3,4};
vector<int>::iterator it;
// 遍历前重新初始化迭代器
for (it = v.begin(); it != v.end(); it++) {
cout << *it << " ";
}
cout << endl; // 输出:0 1 2 3 4
v.erase(v.begin() + 3); // 删除元素3
// 再次遍历前重新初始化迭代器
for (it = v.begin(); it != v.end(); it++) {
cout << *it << " ";
}
cout << endl; // 输出:0 1 2 4
return 0;
}
4. 算法
算法是独立的函数模板,需包含头文件#include ,通过迭代器操作容器数据。
十一、IO流
1. 从C到C++的流演进
IO 流是 “输入 / 输出流” 的简称,本质是通过 “流对象” 操作文件的读写、打开与关闭。不同语言的实现逻辑相似,但语法不同:
2. 字符流和字节流
- 字符流:默认按 ASCII 码转换(比如把字符转成整数存,或整数转字符显示),适合文本文件(.txt)。
- 字节流(二进制流):不转换数据,直接存二进制,适合图片、视频、自定义结构体等非文本文件,C++ 中用ios::binary标识。
3. C++文件操作四步走
所有文件操作都遵循 “创建对象→打开文件→读写→关闭文件” 的流程,不同读写方式适用于不同场景:
方法一:
输入输出运算符(<>)—— 适合简单文本
- 优点:语法直观,像 cout/cin 一样用。
- 缺点:需严格匹配数据格式(比如字符串和数字混用时易读错),慎用!
#include <fstream>
using namespace std;
struct Student { // 自定义结构体
int id;
char name[20];
// 重载>>和<<,让运算符能处理Student类型
friend istream& operator>>(istream& in, Student& s) {
in >> s.id >> s.name; // 按格式读
return in;
}
friend ostream& operator<<(ostream& out, const Student& s) {
out << s.id << " " << s.name; // 按格式写
return out;
}
};
int main() {
fstream file;
Student s = {1, "葫芦娃1"};
// 1.打开文件(写模式+二进制)
file.open("a.txt", ios::out | ios::binary);
// 2.写数据
file << s << endl;
// 3.关闭文件
file.close();
// 读数据
file.open("a.txt", ios::in | ios::binary);
file >> s;
cout << s; // 输出:1 葫芦娃1
file.close();
return 0;
}
方法二:
read()/
write()—— 适合二进制数据(推荐)
- 优点:直接按内存字节读写,精准不混乱,适合结构体、图片等。
- 核心语法:
- 写:file.write((char*)&数据, 数据大小);(需强转成 char*)
- 读:file.read((char*)&接收变量, 数据大小);
- 关键改进:上面的示例中,把file << s换成file.write((char*)&s, sizeof(Student)),读的时候用file.read((char*)&s, sizeof(Student)),无需担心格式问题。
方法三:
get()/
put()—— 适合单字符 / 字节拷贝
- 用途:逐个字节读写,比如实现文件拷贝(把 A 文件的字节一个个读到 B 文件)。
- 示例场景:拷贝一张图片,代码核心逻辑:
fstream srcFile("pic.jpg", ios::in | ios::binary); // 源文件
fstream dstFile("copy_pic.jpg", ios::out | ios::binary); // 目标文件
unsigned char c;
while (1) {
c = srcFile.get(); // 读1个字节
if (srcFile.eof()) break; // 读到文件末尾就停
dstFile.put(c); // 写1个字节
}
srcFile.close();
dstFile.close();
方法四:
seekg()/
seekp()—— 移动文件指针
- 作用:像 “书签” 一样定位文件中的位置(读指针用seekg,写指针用seekp)。
- 常用参数:
- 起始位置:ios::beg(文件开头)、ios::cur(当前位置)、ios::end(文件末尾)
- 示例:获取文件大小
file.seekg(0, ios::end); // 指针移到文件末尾
int fileSize = file.tellg(); // 读取当前指针位置(即文件大小)
file.seekg(0, ios::beg); // 指针移回开头,准备后续读写
十二、异常
1. 为什么需要异常
传统错误处理(如函数返回 - 1、NULL)的问题:代码混乱(到处是 if 判断错误)、错误无法跨函数传递。
异常的优势:把 “错误检测” 和 “错误处理” 分开,代码更清晰。
2. 异常的核心语法:try-throw-catch

#include <iostream>
#include <fstream>
using namespace std;
int main() {
try {
fstream file("nonexist.txt", ios::in);
if (!file.is_open()) {
throw "文件不存在!"; // 抛出string类型异常
}
}
catch (const char* errMsg) { // 捕获string异常
cout << "错误:" << errMsg << endl; // 输出:错误:文件不存在!
}
catch (...) { // 兜底,抓其他类型异常
cout << "未知错误!" << endl;
}
return 0;
}
3. 多层异常处理
异常会从 “抛出点” 向上层函数传递,直到被捕获,类似现实中 “街道→区→市→省” 的问题上报:
void test() {
throw 123; // 抛出int异常,test()自己不处理
}
void f() {
try {
test(); // 调用test(),触发异常
}
catch (float) { // 不匹配int,异常继续向上传
cout << "抓float异常";
}
}
int main() {
try {
f(); // 异常传到main()
}
catch (int errCode) { // 匹配int,处理异常
cout << "捕获到异常码:" << errCode << endl; // 输出:123
}
return 0;
}
4. 标准异常
C++ 提供
std::exception基类,常用派生类:
- bad_alloc:new 分配内存失败时抛出(比如申请太大的内存)。
- out_of_range:访问容器越界(比如 vector [100] 但实际只有 5 个元素)。
- runtime_error:运行时错误(如逻辑没问题,但环境出错)。
示例:捕获内存分配失败
#include <exception> // 需包含此头文件
using namespace std;
int main() {
try {
// 申请超大内存,大概率失败
int* p = new int[10000000000];
}
catch (bad_alloc& e) { // 捕获bad_alloc异常
cout << "内存不够:" << e.what() << endl; // e.what()返回错误描述
}
return 0;
}
5. 自定义异常
(1)继承std::exception或其派生类(如runtime_error);
(2)重写what()函数(返回错误描述)。
#include <exception>
#include <string>
using namespace std;
// 自定义异常类
class FileError : public runtime_error {
public:
// 构造函数,传错误信息
FileError(const string& msg) : runtime_error(msg) {}
// 重写what()
const char* what() const noexcept override {
return ("文件错误:" + runtime_error::what()).c_str();
}
};
int main() {
try {
throw FileError("打开失败"); // 抛出自定义异常
}
catch (FileError& e) {
cout << e.what() << endl; // 输出:文件错误:打开失败
}
return 0;
}
十三、类型转换
C++ 提供 4 种显式类型转换,比 C 语言的(类型)表达式更安全、可读性更高。
1. 四种类型转换对比
(1)static_cast:
普通类型转换(如 int→double、子类→基类指针)
较安全(编译时检查)
int a = static_cast(3.14);
(2)const_cast:
移除
const
属性(仅用于指针 / 引用)
需谨慎(可能改只读数据)
const char* p = "abc";
char* q = const_cast(p);
(3)reinterpret_cast:底层强制转换(如指针→整数、不同类型指针互转)
不安全(完全依赖程序员)
int a = 10;
long addr = reinterpret_cast(&a);
(4)dynamic_cast:
基类 / 子类指针 / 引用转换(需多态支持)
安全(运行时检查,失败返回NULL)
Base* b = new Derived();
Derived* d = dynamic_cast(b);
2. 注意事项
(1)const_cast
别乱改只读数据:
比如
const int a=5; int& b=const_cast(a); b=10;,编译能过,但运行时可能崩溃(因为
a可能存在只读内存中)。
(2)dynamic_cast
需多态:基类必须有虚函数(如
virtual void func(){};),否则编译报错。
(3)优先用 C++ 转换,少用 C 风格:比如用
static_cast(3.14)比
(int)3.14更易读,且编译器能做更多检查。
11万+

被折叠的 条评论
为什么被折叠?



