目录
5. new 和 malloc 的区别
- new 的返回值不需要强制转换,malloc 的返回值需要强制转换
- new 是运算符可以重载(重载是静态多态引导说多态),malloc 是库函数(malloc 不能重载因为他是c语言的库函数,可以说c 和 c++ 编译方式的区别)
- new不需要传入想要申请的具体字节个数,malloc 需要传入想要申请的具体字节个数
- malloc 申请失败例如传入参数是负数的情况下返回空,mew申请失败会抛出异常(引出异常机制,以及自定义异常类如何实现)
- 如果给一个类分配堆区内存 new 会先调用 malloc 分配内存,然后再调用构造函数给成员变量赋值(注意:构造函数给成员变量赋值的 可以引出 初始化参数列表是给成员变量初始化)
- new 申请的堆区内存需要使用 delete 释放,delete 先调用析构函数再调用free(free也是c语言库函数和 malloc是配套的) 这里可以说一下为什么先调用析构再调用free
#include<iostream>
using namespace std;
int main()
{
//malloc申请内存
int *p = (int*)malloc(8);//在堆区申请八个字节的内存
//malloc申请失败
int*p1 = (int*)malloc(-6);
if (p1 == NULL) cout << "malloc申请失败" << endl;
//new分配内存
int *n = new int(3); //单个元素
int *n1 = new int[3]; //数组
return 0;
}
异常处理机制:
C++ 的异常处理机制允许程序在运行时检测错误,并通过 抛出(throw) 和 捕获(catch) 异常来优雅地处理错误,而不是直接崩溃。
核心关键字
-
try
:定义可能抛出异常的代码块。 -
throw
:抛出异常(可以是任意类型,如int
、string
或自定义类)。 -
catch
:捕获并处理异常。
示例(基本异常处理)
#include <iostream>
using namespace std;
int main() {
try {
int age = -1;
if (age < 0) {
throw runtime_error("年龄不能为负数!"); // 抛出异常 runtime_error 是 C++ 标准库中定义的一个异常类
}
} catch (const runtime_error& e) { // 捕获异常
cout << "错误: " << e.what() << endl;
}
return 0;
}
runtime_error
是 C++ 标准库中定义的一个异常类 ,用于表示程序运行时发生的错误(如逻辑错误、无效输入、资源不足等)。它继承自 std::exception
,是 C++ 异常体系的核心组成部分。
允许传递详细的错误信息(通过 what()
获取)what作用返回错误描述的c风格字符串(const char*)。所有标准异常类(包括自定义继承的)必须实现此方法。
输出:
错误: 年龄不能为负数!
其他标准异常类
C++ 在 <stdexcept>
中还提供了其他常见异常类:
异常类 | 用途 |
---|---|
std::logic_error | 程序逻辑错误(如无效参数) |
std::out_of_range | 数组/容器越界访问 |
std::bad_alloc | new 内存分配失败 |
std::invalid_argument | 函数参数无效 |
自定义异常类:
1. 选择基类
通常继承自exception
类(需包含 <stdexcept>)
或它的派生类(如runtime_error
、logic_error
等 )。exception
是 C++ 标准库中所有异常类的基类,它提供了虚函数what()
,用于返回描述异常的字符串。选择合适的基类,能让自定义异常类融入 C++ 标准异常体系,方便统一处理。
2. 定义异常类
以继承自exception
为例,定义自定义异常类,示例如下:
#include <exception>
#include <string>
#include <iostream>
using namespace std;
// 自定义异常类
class MyException : public exception {
private:
string m_message; //用于存储异常信息
public:
//构造函数,接收异常信息作为参数
MyException(const string& message) : m_message(message) {}
//重写what函数,返回异常信息
const char* what() const noexcept override {
return m_message.c_str();
}
};
在这个示例中:
- 声明了一个私有成员变量
m_message
,用于保存具体的异常描述信息。 - 定义了构造函数
MyException(const string& message)
,通过传入的参数初始化m_message
。 - 重写了
exception
基类的what()
函数,返回异常信息的 C 风格字符串(c_str()
)。noexcept
表示该函数不会抛出异常,override
关键字用于明确表明是重写基类虚函数,增强代码可读性和编译器检查力度。
3. 抛出异常
在程序中合适的位置,当满足特定错误条件时,创建自定义异常类的对象并使用throw
关键字抛出。例如:
// 除法函数,可能抛出异常
void divide(int a, int b) {
if (b == 0) {
throw MyException("除数不能为0"); //抛出异常
}
cout << a / b << endl;
}
这里divide
函数进行除法运算,若除数b
为 0,就创建MyException
异常对象并抛出,携带 “除数不能为 0” 的异常信息。
4. 捕获并处理异常
使用try-catch
块捕获并处理自定义异常。示例如下:
// 主函数
int main() {
try {
divide(10, 0);
} catch (const MyException& e) {
cerr << "捕获到异常: " << e.what() << endl;
}
return 0;
}
在try
块中调用可能抛出异常的divide
函数,若divide
函数抛出MyException
异常,catch
块会捕获该异常对象(这里以常量引用形式捕获,避免不必要的对象拷贝 ),并通过调用what()
函数获取异常信息进行处理,这里只是简单输出异常信息到标准错误流cerr
。
new
的异常行为
当 new
分配内存失败时,默认抛出 std::bad_alloc
异常(而非返回 NULL
)。
示例(捕获 new
的异常)
#include <iostream>
#include <new> // 包含 bad_alloc 定义
using namespace std;
int main() {
try {
int* p = new int[1000000000000]; // 尝试分配超大内存
} catch (const bad_alloc& e) {
cout << "内存分配失败: " << e.what() << endl;
}
return 0;
}
输出:
内存分配失败: std::bad_alloc
new
与 malloc
的异常对比
行为 | new | malloc |
---|---|---|
失败处理 | 抛出 std::bad_alloc 异常 | 返回 NULL |
自定义异常 | 可通过 try-catch 捕获 | 需手动检查 if (ptr == NULL) |
适用场景 | C++ 面向对象代码 | C 或底层内存操作 |
初始化参数列表
作用:
c++提供了初始化列表语法,用来初始化属性
语法:构造函数():属性1(值1),属性2(值2)...{}
- 构造函数是给成员变量赋值,不是初始化
- 初始化参数列表是给成员变量初始化的
- 初始化参数列表只能在构造函数中使用
- 初始化的顺序和初始化参数列表的顺序无关,和成员变量的顺序一致
- 常量(const)和引用必须在初始化参数列表中初始化
示例:
#include<iostream>
#include<string>
using namespace std;
class A
{
public:
int a;
int b;
int c;
const int d;
int& e;
A(int c1, int b1, int d, int e) :a(c),b(b1),c(c1),d(d),e(e)
{
cout << a <<" " << b<<" " << c << endl;
}
};
int main()
{
A a(1,2,3,4);
}
#include<iostream>
using namespace std;
class A
{
int a, b, c;
public:
A(int b1, int c1) :c(c1), a(c),b(b1) //初始化的顺序和初始化参数列表中的顺序无关
{
cout << a << " " << b << " " << c << endl;
}
};
int main()
{
A a(1,2);
}
为什么先调用析构再调用free
析构函数是用于释放对象内部在生命周期中所占用的资源。
free是c语言标准库函数,用来释放对象本身占用的堆内存空间。
而先调用析构再调用free主要是因为:如果先调用free释放对象内存,那么对象就不再存在,此时对象内部的资源无法再通过对象的析构函数释放,会导致资源泄露。
6. 构造函数
- 创建对象的时候编译器自动调用构造函数,无需手动调用,而且只会调用一次
- 构造函数可以有参数,因此可以发生重载
- 构造函数和类名相同,没有返回值也不写void
- 如果没有实现构造函数编译器会提供默认的无参构造函数
- 构造函数时给成员变量赋值的不是初始化(引出初始化参数列表)
- 还可以了解一下移动构造,继承构造,委托构造
构造函数语法:类名(){}
#include<iostream>
using namespace std;
class A
{
public:
int num;
char pri;
A()
{
cout << "A的默认构造" << endl;
}
A(int n,char p)
{
num = n;
pri = p;
cout << "有参构造" << endl;
}
};
int main()
{
A a;//默认构造
A a1(2,3);//有参构造
cout << sizeof A << endl; //8 int是4 char 是1 对齐机制 输出8
return 0;
}
构造函数的分类及调用
两种分类方式:
按参数分类:有参构造和无参构造
按类型分为:普通构造和拷贝构造
三种调用方式:
括号法
显示法
隐式转换法
示例:
#include<iostream>
using namespace std;
class A
{
public:
int* p;
A()
{
p = nullptr;
cout << "A的默认构造" << endl;
}
/*explicit*/ A(int num)
{
cout << "调用有参构造" << endl;
p = new int(num);
}
};
void test01() //括号法
{
//A a(); // 不是调用无参构造,编译器会认为是函数声明
A a; //调用无参构造
A a1(2); //调用有参构造
}
void test02()//显示法
{
A a = A(); // 调用无参构造
A a1 = A(2);// 调用有参构造
}
void test03()//隐式法
{
A a = 2; //隐式法调用有参构造,explicit 可以避免构造函数隐式法调用
}
int main()
{
test03();
return 0;
}
练习:
创建一个person
类,其中属性包含:姓名,性别,年龄,循环输入 3 个人并且打印出每个人的信息。
#include<iostream>
#include<string>
#include<vector>
using namespace std;
class Person
{
string name;
string sex;
int age;
public:
Person()
{
cin >> name >> sex >> age;
}
void Print_Person()
{
cout << name << " " << sex << " " << age << endl;
}
};
int main()
{
vector<Person> vec = {Person(), Person() ,Person() };
for (auto it : vec)
{
it.Print_Person();
}
return 0;
}
构造函数调用规则
默认情况下,c++编译器至少给一个类添加3个函数
- 默认构造函数(无参,函数体为空)
- 默认析构函数(无参,函数体为空)
- 默认拷贝构造函数,对属性进行值拷贝
构造函数调用规则如下:
- 如果用户定义有参构造函数,c++不再提供默认无参构造,但是会提供默认拷贝构造
- 如果用户定义拷贝构造函数,c++不会再提供其他构造函数
示例:
class Person {
public:
//无参(默认)构造函数
Person() {
cout << "无参构造函数!" << endl;
}
//有参构造函数
Person(int a) {
age = a;
cout << "有参构造函数!" << endl;
}
//拷贝构造函数 参数为引用 避免递归,加const 避免修改实参
Person(const Person& p) {
age = p.age;
cout << "拷贝构造函数!" << endl;
}
//析构函数
~Person() {
cout << "析构函数!" << endl;
}
public:
int age;
};
void test01()
{
Person p1(18); //调用自己写的有参构造
//如果不写拷贝构造,编译器会自动添加拷贝构造,并且做浅拷贝操作
Person p2(p1); //调用自己写的拷贝构造
cout << "p2的年龄为: " << p2.age << endl;
}
void test02()
{
//如果用户提供有参构造,编译器不会提供默认构造,会提供拷贝构造
Person p1; //此时如果用户自己没有提供默认构造,会出错
Person p2(10); //用户提供的有参
Person p3(p2); //此时如果用户没有提供拷贝构造,编译器会提供
//如果用户提供拷贝构造,编译器不会提供其他构造函数
Person p4; //此时如果用户自己没有提供默认构造,会出错
Person p5(10); //此时如果用户自己没有提供有参,会出错
Person p6(p5); //用户自己提供拷贝构造
}
int main() {
test01();
return 0;
}
this
一个对象的 this 指针并不是对象本身的一部分,不会影响 sizeof(对象)的结果。this 作用域是在类内部,当在类的非静态成员函数中访问类的非静态成员的时候,编译器会自动将对象本身的地址作为一个隐含参数传递给函数。
this 是指向当前对象的指针,哪个对象调用包含 this 指针的函数, this 指向哪个对象。
this 一般在构造函数中使用,用来区分成员变量和参数。
#include<iostream>
#include<string>
#include<vector>
using namespace std;
class A
{
int n;
int* p;
public:
A()
{
n = 0;
p = nullptr;
}
A(int n)
{
this->n = n; //使用this指针来区分成员变量和参数
p = new int[n];
}
};
int main()
{
A a(2);
A b(3);
A c(4);
return 0;
}
7. 析构函数
主要作用在于对象销毁前系统自动调用,执行一些清理工作
- 对象被释放的时候编译器会自动调用析构函数,无需手动调用,而且只会调用一次
- 析构函数名为 ~+ 类名,没有返回值 也不写void
- 析构函数不可以有参数,因此不可以发生重载
- 如果没有实现析构函数编译器会提供默认的析构函数
- 析构函数是释放对象里面成员变量指向的堆区内存的
如下代码:
class A
{
int *p = nullptr;
public:
A(int n) //实现了构造函数编译器不提供默认的无参构造
{
if (n > 0) p = new int[n]; //在堆区分配内存 n 个int类型的数据
}
~A()
{
if (p) delete[]p; // 注意释放数组时 一定要加上[],否则只会释放数组的第一个元素,造成内存泄露
}
};
8. 拷贝构造
拷贝构造什么时候被调用?
- 用已经存在的对象初始化新的对象时
- 值传递的方式给函数参数传值
- 以值方式返回局部对象
1. 使用一个已经创建完毕的对象来初始化一个新的对象
当定义一个新对象,并使用同类型的已存在对象对其进行初始化时,会调用拷贝构造函数 。比如:
class Person {
public:
int age;
Person(int a) : age(a) {} //这里用了初始化参数列表来初始化
Person(const Person& p) : age(p.age) {
cout << "调用拷贝构造函数" << endl;
}
};
Person p1(10);
Person p2(p1); // 这里调用拷贝构造函数,用p1初始化p2
在Person p2(p1);
这行代码中,编译器会调用Person
类的拷贝构造函数,将p1
对象的属性值(这里是age
)复制给新创建的p2
对象 。此外,Person p2 = p1;
这种初始化方式同样会调用拷贝构造函数 ,尽管语法形式上像赋值,但本质是初始化新对象。
2. 值传递的方式给函数参数传值
当以值传递的方式将对象作为参数传递给函数时,会调用拷贝构造函数 。因为值传递时,函数会创建一个实参对象的副本(形参 ),这个创建副本的过程需要调用拷贝构造函数 。示例如下:
class Person {
public:
int age;
Person(int a) : age(a) {}
Person(const Person& p) : age(p.age) {
cout << "调用拷贝构造函数" << endl;
}
};
void func(Person p) {//当形参为值传递的时候,实参初始化形参会调用拷贝构造函数
// 函数内部操作
}
Person p1(20);
func(p1);
3. 以值方式返回局部对象
当函数以值的方式返回一个局部对象时,会调用拷贝构造函数 。由于函数结束后,局部对象的生命周期本应结束,为了能将对象的值返回给调用者,编译器会创建一个临时对象(匿名对象 ),并通过拷贝构造函数将局部对象的值复制到这个临时对象中返回 。例如:
class Person {
public:
int age;
Person(int a) : age(a) {}
Person(const Person& p) : age(p.age) {
cout << "调用拷贝构造函数" << endl;
}
};
Person func() {
Person p(30); // 局部对象
return p; // 调用拷贝构造函数创建临时对象,将p的值复制过去并返回
}
Person p2 = func();
在func
函数中,当执行return p;
时,会调用Person
类的拷贝构造函数,创建一个临时对象,把局部对象p
的值复制到临时对象中,然后将临时对象返回给调用者 ,最终赋值给p2
。
构造函数的参数不是引用行么?
不行,拷贝构造的参数如果不是引用会导致无限递归,前面加 const 是为了避免通过形参修改实参。(无限递归:每次调用拷贝构造函数都需要先复制实参,而复制实参又需要调用拷贝构造函数,形成无线递归)
深拷贝和浅拷贝有什么区别
- 深拷贝:拷贝相同大小的内存并且数值也相同,就是在堆区重新申请空间,进行拷贝操作
- 浅拷贝:简单的赋值操作,编译器提供的拷贝构造是浅拷贝
浅拷贝:简单复制对象的成员变量,对于指针类型的成员变量只是复制指针的值,多个对象可能共享同一块内存,容易导致内存管理问题,如双重释放。 深拷贝:为对象的指针成员分配新的内存,并复制其内容,各个对象的内存相互独立,避免了内存管理问题,但会增加内存开销。在涉及动态内存分配的类中,通常需要自定义拷贝构造函数和赋值运算符重载来实现深拷贝。
示例:
#include<iostream>
using namespace std;
class A
{
int num;
int* p;
public:
A()
{
num = 0;
p = nullptr;
}
A(int a)
{
num = a;
p = new int(num);
}
A(const A& other)
{
//浅拷贝就是简单的赋值操作
/*this->num = other.num;
this->p = other.p;*/
//深拷贝 :不同对象指向的堆区内存不同,但是大小 和 值相同
this->num = other.num;
this->p = new int(*other.p);
cout << "拷贝构造" << endl;
}
~A() //析构函数会在对象被销毁的时候调用
{
if (p) delete p;
}
};
int main()
{
A a(2);
A b = a;
return 0;
}
总结:如果属性有在堆区开辟的,一定要自己提供拷贝构造函数,防止浅拷贝带来的问题
拷贝构造练习
创建一个汽车类包含:汽车使用年龄,剩余燃油(fuel 在堆区分配),并实现有参构造和拷贝构造。
#include<iostream>
using namespace std;
class car
{
public:
int age;
int* fule;
car()
{
age = 0;
fule = nullptr;
}
car(int age, int f)
{
this->age = age;
fule = new int(f);
}
car(const car &other)
{
this->age = other.age;
this->fule = new int(*other.fule);
}
~car()
{
if (fule) delete fule;
}
void printf_car()
{
cout << "汽车使用年限为" << age << " 剩余燃油: " << *fule << endl;
}
};
int main()
{
car c(2,200);
car c1 = c;
c.printf_car();
c1.printf_car();
return 0;
}
持续更新......