我是靠谱客的博主 愤怒红酒,这篇文章主要介绍「地表最强」C++核心编程(五)类和对象--对象初始化和清理一、构造函数和析构函数二、构造函数的分类及调用三、拷贝构造函数调用时机四、构造函数调用规则五、深拷贝与浅拷贝六、初始化列表七、类对象作为类成员八、静态成员,现在分享给大家,希望可以做个参考。

环境:
编译器:CLion2021.3;操作系统:macOS Ventura 13.0.1

文章目录

  • 一、构造函数和析构函数
    • 1.1 构造函数
    • 1.2 析构函数
    • 1.3 示例
  • 二、构造函数的分类及调用
    • 1.1 构造函数的分类
    • 1.2 构造函数的调用
  • 三、拷贝构造函数调用时机
    • 3.1 调用时机
    • 3.2 返回值优化
  • 四、构造函数调用规则
  • 五、深拷贝与浅拷贝
    • 5.1 浅拷贝
    • 5.2 深拷贝
  • 六、初始化列表
  • 七、类对象作为类成员
  • 八、静态成员
    • 8.1 静态成员变量
    • 8.2 静态成员函数

地表最强C++系列传送门:
「地表最强」C++核心编程(一)内存分区模型
「地表最强」C++核心编程(二)引用
「地表最强」C++核心编程(三)函数提高
「地表最强」C++核心编程(四)类和对象----封装
「地表最强」C++核心编程(五)类和对象----对象初始化和清理
「地表最强」C++核心编程(六)类和对象----对象模型和this指针
「地表最强」C++核心编程(七)类和对象----友元
「地表最强」C++核心编程(八)类和对象----运算符重载
「地表最强」C++核心编程(九)类和对象----继承
「地表最强」C++核心编程(十)类和对象----多态
「地表最强」C++核心编程(十一)文件操作

一、构造函数和析构函数

构造函数和析构函数是必须实现的两个函数,即使自己没有写,编译器也会默认调用默认的,空的构造和析构函数。

1.1 构造函数

构造函数主要作用在于创建对象时为对象的成员属性赋值,构造函数由编译器自动调用,无须手动调用。

语法: 类名(){}

构造函数的特点:

  • 没有返回值,不用写void
  • 函数名与类名相同
  • 构造函数可以有参数,可以发生重载
  • 创建对象时自动调用,且只调用一次

1.2 析构函数

析构函数主要作用在于对象销毁前系统自动调用,执行一些清理工作。

语法: ~类名(){}

析构函数的特点:

  • 没有返回值,不用写void
  • 函数名与类名相同,前面加个~
  • 析构函数不可以有参数,不可以发生重载
  • 销毁对象时自动调用,且只调用一次

1.3 示例

class Person {
public:
    Person() {
        cout << "Person的构造函数" << endl;//自己写了就自动调用自己写的。否则就调用默认的空的构造函数
    }

    ~Person() {
        cout << "Person的析构函数" << endl;
    }
};

//构造和析构函数都必须实现,自己不提供编译器会提供一个空的构造和析构函数
void test01(){
    Person p;
}


int main() {
    test01();
    return 0;
}

二、构造函数的分类及调用

1.1 构造函数的分类

构造函数按照不同的标准可以分为不同的类型:
按参数分类可分为无参构造函数(也叫做默认构造函数)有参构造函数
按类型分类可分为普通构造函数拷贝构造函数
⚠️由于创建对象需要调用构造函数,因此构造函数的权限应该是public,否则类外无法调用构造函数,也就无法实例化对象。
⚠️拷贝构造函数的参数需要是常量对象

class Person {

public://要加public作用域,否则默认是private,就无法调用构造析构函数,也就无法创建对象
    int age;

    Person() {
        cout << "Person的无参构造函数" << endl;
    }

    Person(int a) {
        age = a;
        cout << "Person的有参构造函数" << endl;
    }

    Person(const Person &p) {//拷贝构造函数,要用const和&的方式传参
        age = p.age;//将传入的人身上的所有属性,拷贝到自己身上
        cout << "Person的拷贝构造函数" << endl;
    }

    ~Person() {
        cout << "Person的析构函数" << endl;
    }
};

1.2 构造函数的调用

构造函数的调用有三种方法,分别是括号法、显示法和隐式转换法

void test() {
    //括号法
    Person p1;//默认构造函数调用
    Person p2(10);//有参构造函数调用
    Person p3(p2);//拷贝构造函数调用
    //⚠️括号法调用默认构造函数的时候不要加(),否则编译器会认为这是一个函数的声明
//    Person p1();//虽然不报错,但是逻辑错误,编译器会认为有一个返回值类型是Person,名为p1的函数
    cout<<"p2的年龄是:"<<p2.age<<endl;//10
    cout<<"p3的年龄是:"<<p3.age<<endl;//10
    
    
    //显示法
    Person p1;
    Person p2 = Person(10);//有参构造,实际上是将匿名对象赋值给p2
    Person p3 = Person(p2);//拷贝构造
//    Person(10);//这是一个创建匿名对象,但没有办法使用        特点:当前行执行结束后,系统会立即回收掉匿名对象
//    cout<<"匿名对象已经没了"<<endl;

    //⚠️不要用拷贝构造函数初始化匿名对象,编译器会认为这是重定义。   Person(p3); 会被转换为 Person p3;
//    Person(p3);//err,Redefinition of 'p3'


    //隐式转换法
    Person p4 = 10;//相当于Person p4 = Person(10);     有参构造
    Person p5 = p4;//相当于Person P5 = Person(p4);     拷贝构造
}

三、拷贝构造函数调用时机

3.1 调用时机

C++中拷贝构造函数调用时机通常有三种情况:

  • 1使用一个已经创建完毕的对象来初始化一个新对象
  • 2值传递的方式给函数参数传值
  • 3以值方式返回局部对象
//1使用一个已经创建完毕的对象来初始化一个新对象
void test01() {
    Person p1(20);//括号法调用构造函数
    Person p2(p1);//用对象p1来初始化p2,实际上就是调用了拷贝构造函数
    cout << "p2的年龄:" << p1.mAge << endl;
}


//2值传递的方式给函数参数传值
void doWork1(Person p) {}
void test02() {
    Person p;//调用默认构造函数
    doWork1(p);//值传递,doWork函数会调用拷贝构造函数给形参临时开辟空间
}


//3以值方式返回局部对象
Person doWork2() {
    Person p;//调用默认构造函数
    return p;//返回的不是上边的p,因为已经被释放掉了。这里返回的是另一个创建的对象,这个对象是用拷贝构造函数创建的。
}
void test03() {
    Person p = doWork2();//CLion测试下不会调用析构函数,这是因为返回值优化技术。
    cout << &p << endl;
}

3.2 返回值优化

这里解释一下第3点以值的方式返回对象:doWork2函数中首先定义了一个Person类型变量p,我们假设这个对象的地址是add1,然后该函数返回了对象p。这一步的操作实际上是用拷贝构造函数的方法又创建了一个对象p’,我们假设p’的地址是add2,然后把p作为拷贝构造函数的参数传给p’。而实际返回的是p’而不是p,这两个对象的地址是不同的,是两个对象
返回值优化是一项编译优化技术,使得返回对象时不必调用拷贝构造函数,经过测试后返回的就是已经创建好的p的地址而不会产生p’。
关于返回值优化,更多的可以参考一下这里:Return value optimization-Wikipedia

四、构造函数调用规则

默认情况下,c++编译器至少给一个类添加3个函数:

  • 默认构造函数(无参,函数体为空)
  • 默认析构函数(无参,函数体为空)
  • 默认拷贝构造函数,对属性进行值拷贝

构造函数调用规则如下:

  • 如果用户定义有参构造函数,c++不在提供默认无参构造,但是会提供默认拷贝构造
  • 如果用户定义拷贝构造函数,c++不会再提供其他构造函数
class Person {
public:
    int age;

    //无参(默认)构造函数
    Person() {
        cout << "无参构造函数!" << endl;
    }
    //有参构造函数
  Person(int a) {
        age = a;
        cout << "有参构造函数!" << endl;
    }
    //拷贝构造函数
    Person(const Person &p) {
        age = p.age;
        cout << "拷贝构造函数!" << endl;
    }

    //析构函数
    ~Person() {
        cout << "析构函数!" << endl;
    }
};

void test(){
    Person p;//若只写有参构造或者拷贝构造,不写无参,此处会报错。因为此行代码会调用无参,但是我们写了有参,编译器就不会提供无参
    Person p(28);//若没写无参和有参,只写了拷贝构造,此处会报错。因为写了拷贝构造,编译器就不会提供其他无参和有参
    Person p2(p);
    cout << "p2的年龄:" << p2.age << endl;
}

五、深拷贝与浅拷贝

5.1 浅拷贝

浅拷贝就是简单的赋值操作,默认的拷贝构造函数进行的就是浅拷贝。

class Person{
private:
	string m_name;
	int m_age;
public:
	Person(const Person& p){//这里其实就是浅拷贝
		m_name = p.m_name;
		m_age = p.m_age;
	}

5.2 深拷贝

从下边这个实例说起,注意这里的身高我定义成了指针,指向的内容是身高

class Person {
public:
    int m_age;
    int *m_height;//指针,指向的内容是身高

public:
    //有参构造函数
    Person(int age, int height) {
        m_age = age;
        m_height = new int(height);//用new创建在堆区
    }

    //拷贝构造函数
    Person(const Person &p) {
        m_age = p.m_age;
        m_height = p.m_height;//默认的浅拷贝
    }

    //析构函数
    ~Person() {//堆区开辟的数据在此时可以释放了
        if (m_height != NULL) {
            delete m_height;
            m_height = NULL;
        }
    }
};

void test01() {
    Person p1(18, 180);//调用有参构造
    Person p2(p1);//调用拷贝构造
}

在这里插入图片描述
如图,通过有参构造创建了p1,然后通过浅拷贝的拷贝构造创建了p2。而浅拷贝的只是在机械的赋值,因此p2的所有属性都和p1一样,此时他们的身高的指针都是指向同一堆空间的,那么此时他们中任何一个对这块空间的操作会直接影响到另一个人。在test()调用结束后,这两个对象也会调用自己的析构函数来销毁空间。p2调用了自己的析构函数,将0x003这块儿空间释放掉,然后p1才调用自己的析构函数(这里涉及到析构函数的调用时间,简单来说就是先构造的后析构,想想栈的特性就明白了),但此时此空间已经被释放掉了,还要释放那就是非法的。
想解决这个问题,只需要在构造p2的时候,让他的身高指针指向与p1不同的空间即可。因此只需重写拷贝构造函数即可:

    Person(const Person &p) {
        //如果不利用深拷贝在堆区创建新内存,会导致浅拷贝带来的重复释放堆区问题。若不自己重写拷贝构造函数,拷贝构造出来的对象和被拷贝的对象指向同一块儿空间。
        m_age = p.m_age;
        m_height = new int(*p.m_height);//重新申请一块堆区,属于深拷贝
    }

在这里插入图片描述
这样就不会有重复释放的问题,这就是深拷贝。

六、初始化列表

初始化列表是一种利用构造函数初始化属性的另一种方式。

语法: 构造函数():属性1(值1),属性2(值2)… {}

class Person {
public:
    int m_A;
    int m_B;
    int m_C;

public:
	//传统方式初始化
    Person(int a, int b, int c) {
    	m_A = a;
    	m_B = b;
    	m_C = c;
    }
    //初始化列表方式初始化
    Person() : m_A(10), m_B(20), m_C(30) {}
    Person(int a, int b, int c) : m_A(a), m_B(b), m_C(c) {}
};

七、类对象作为类成员

假设一个类的对象B是另一个类A的成员,那么在调用构造函数的时候,会先调用B的构造函数:

class Phone {
public:
    string m_PhoneName;

    Phone(string name) {
        m_PhoneName = name;
        cout << "Phone构造" << endl;
    }
    ~Phone() {
        cout << "Phone析构" << endl;
    }
};

class Person {
public:
    string m_Name;
    Phone m_Phone;
    //初始化列表可以告诉编译器调用哪一个构造函数             Phone m_Phone = pName;其实是隐式转换法
    Person(string name, string pName) : m_Name(name), m_Phone(pName) {
        cout << "Person构造" << endl;
    }

    ~Person() {
        cout << "Person析构" << endl;
    }

    void playGame() {
        cout << m_Name << " 使用" << m_Phone.m_PhoneName << " 手机! " << endl;
    }
};

void test01() {
    //当类中成员是其他类对象时,我们称该成员为对象成员
    //构造的顺序是 :先调用对象成员的构造,再调用本类构造,析构顺序与构造相反
    Person p("张三", "苹果1024");//先构造phone
    p.playGame();
}

八、静态成员

静态成员就是在成员变量和成员函数前加上关键字static,称为静态成员。包括静态成员变量静态成员函数

8.1 静态成员变量

特点:
1.所有对象共享同一份数据,因此不属于某个对象
2.在编译阶段分配内存,全局区
3.类内声明,类外初始化

class Person {
public:
    static int m_A; //静态成员变量
private:
    static int m_B; //静态成员变量也是有访问权限的
};

//类外初始化,否则无法正常访问
int Person::m_A = 10;
int Person::m_B = 10;//private也需要类外初始化,这是可以的

void test01() {
    //静态成员变量两种访问方式,这是由于静态成员变量被所有对象共享
    //1、通过对象        非静态成员变量只能这么访问
    Person p1;
    p1.m_A = 100;
    cout << "p1.m_A = " << p1.m_A << endl;//100

    Person p2;
    p2.m_A = 200;
    cout << "p1.m_A = " << p1.m_A << endl; //200 共享同一份数据,不属于某个对象
    cout << "p2.m_A = " << p2.m_A << endl; //200

    //2、通过类名        非静态成员变量不能这么访问
    cout << "m_A = " << Person::m_A << endl;
    //cout << "m_B = " << Person::m_B << endl; //err,私有权限访问不到
}

8.2 静态成员函数

特点:
1.所有对象共享同一个函数
2.静态成员函数只能访问静态成员变量

class Person
{
public:
    static int m_A; //静态成员变量
    int m_B; //只属于对象本身,不能被对象共享

    //静态成员函数
    static void func()
    {
        cout << "func调用" << endl;
        m_A = 100;
//        m_B = 100; //err,不可以访问非静态成员变量,无法区分到底是哪个对象的属性
    }

private:
    //静态成员函数也是有访问权限的
    static void func2()
    {
        cout << "func2调用" << endl;
    }
};

int Person::m_A = 10;


void test01()
{
    //静态成员变量两种访问方式
    //1、通过对象
    Person p1;
    p1.func();

    //2、通过类名
    Person::func();
//    Person::func2(); //err,私有权限访问不到
}

最后

以上就是愤怒红酒最近收集整理的关于「地表最强」C++核心编程(五)类和对象--对象初始化和清理一、构造函数和析构函数二、构造函数的分类及调用三、拷贝构造函数调用时机四、构造函数调用规则五、深拷贝与浅拷贝六、初始化列表七、类对象作为类成员八、静态成员的全部内容,更多相关「地表最强」C++核心编程(五)类和对象--对象初始化和清理一、构造函数和析构函数二、构造函数内容请搜索靠谱客的其他文章。

本图文内容来源于网友提供,作为学习参考使用,或来自网络收集整理,版权属于原作者所有。
点赞(56)

评论列表共有 0 条评论

立即
投稿
返回
顶部