【C++ 篇 3 】 new 和 malloc 的区别、构造函数(this)、析构函数、拷贝构造(深拷贝和浅拷贝)

目录

5. new 和 malloc 的区别

 异常处理机制:

自定义异常类:

初始化参数列表

为什么先调用析构再调用free

6. 构造函数

构造函数的分类及调用

构造函数调用规则

this

7. 析构函数

8. 拷贝构造

拷贝构造什么时候被调用?

1. 使用一个已经创建完毕的对象来初始化一个新的对象

2. 值传递的方式给函数参数传值

3. 以值方式返回局部对象

构造函数的参数不是引用行么?

深拷贝和浅拷贝有什么区别

拷贝构造练习


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) 异常来优雅地处理错误,而不是直接崩溃。

核心关键字

  1. try:定义可能抛出异常的代码块。

  2. throw:抛出异常(可以是任意类型,如 intstring 或自定义类)。

  3. 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_allocnew 内存分配失败
std::invalid_argument函数参数无效

自定义异常类:

1. 选择基类

通常继承自exception类(需包含 <stdexcept>)或它的派生类(如runtime_errorlogic_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 的异常对比

行为newmalloc
失败处理抛出 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;
}

持续更新......

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值