• C++核心编程(二)



    前言

    记录C++核心编程(二)

    一、 成员属性私有化

    通过上一篇的总结我们可以知道,class的默认属性是私有属性;
    为什么默认属性是私有的?
    我们知道,假设一个对象是公有的话,那么是不是任何人都能访问并且修改它,是不是就会存在一些潜在的危险,为了避免这些危险,我们通常将一个对象的成员属性设置为私有,设置为私有有什么好处呢?
    1、可以控制成员属性的写和读的权限;
    2、可以在写入数据时,对写入的数据进行检查和筛选,保证数据的有限性;

    我们用一个具体的实例来说话:

    class Person
    {
    private:
    	int m_Age;
    public:
    	void setM_Age(int age)//通过成员函数来设置私有属性的值(控制写)
    	{
    		m_Age = age;
    	}
    	int getM_Age()//同理,通过成员函数来设置私有属性的值(控制读)
    	{
    		return m_Age;
    	}
    
    };
    void test1()
    {
    	Person p1;
    	p1.setM_Age(18);
    	int age = p1.getM_Age();
    	cout << age << endl;
    }
    int main()
    {
    	test1();
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

    在这里插入图片描述
    我们可以发现我们无法直接修改私有属性的值;
    我们也无法直接读取私有属性的值;
    应为该成员属性被设置为私有了,我们根本访都访问不到它,何谈读和写,计算你找到了它,人家不给你“开门”,你啥也做不了!!!
    但是我们的成员函数是公共的,我们可以通过它的成员函数,来间接对其进行修改和读取:
    在这里插入图片描述
    ,如果我们设置成公有的话,那岂不是什么人都能入侵我们的房子,并且还能对我们的房子做一些未知的操作,这不就增加了我们房子的危险性;但是我们将防止设置为私有属性,也就相当于,只留了一条路来进入这个房子,不准你走其它道路,,当然你从我这那东西,也得通过“正规途径”,你看这样是不是方便管理和控制了;因此我们在创建一个类的时候,通常将成员属性设置为私有,并且提供一些公共的接口去管理这些私有属性;
    从上面的代码我们可以看到,一个人的年龄不可能超过150岁,也不可能为负数吧,于是我们可以在写数据的时候,对年龄这个属性进行一些限制,比如:
    在这里插入图片描述
    在这里插入图片描述
    这也体现了设置私有属性的第二个好处;

    二、对象的初始化和清理

    在日常生活中,我们买到了新手机,是不是发现都是我们手机的语言都是简体中文(在大陆买),在国外买的话,手机语言是不是又是其它语言了,这是不是就是对一个手机的初始化呢?当然在我们对于不用的手机,我们通常会拿去卖掉,或者直接砸掉,但是我们在做这些之前是不是应该格式化一下手机,清除一下手机的数据,以此来保护我们的隐私;
    对于一个对象来说也是这样的,我们在创建对象的时候,编译器就会自动掉一个叫做构造函数的东西,帮我们完成对一个对象的初始化,同理在对象销毁的时候,也会自动掉一个叫做析构函数的东西将我们对象里面东西给清除掉;
    对象的初始化和清理工作是编译器强制我们做的,如果我们不给一个类提供构造函数和析构函数的话,编译器会为我们提供,但是编译器所提供的函数都是无函数体的(也就是空实现,函数体里面啥也没有),因此我们在设计一个类的时候,同时需要考虑构造函数和析构函数的设计;

    1、构造函数

    语法:类名+()
    1、没有返回值,也不用写void;
    2、函数名和类名一样;
    3、构造函数可以发生重载技术
    4、程序在调用对象时会自动调用构造函数(也就是我们创建对象的时候),无需自动调用,而且只会调用一次,(但是我们可以指定调用的构造函数的类型);
    
    • 1
    • 2
    • 3
    • 4
    • 5

    2、析构函数

    语法:~类名+()
    1、没有返回值,也不写void;
    2、函数名和类名一样,但是要在函数名前面叫一个~;
    3、析构函数不能有参数,因此不能发生重载技术;
    4、程序在对象销毁前会自动调用析构函数,无需手动调用,而且只会调用一次;
    
    • 1
    • 2
    • 3
    • 4
    • 5

    接下来我们来具体看看语法的实现:

    class Person
    {
    private:
    	string m_name;
    	int m_age;
    public:
    	Person()
    	{
    		cout << "构造函数的调用" << endl;
    	}
    	~Person()
    	{
    		cout << "析构函数的调用" << endl;
    	}
    
    };
    void test2()
    {
    	Person p1;//我们没有调用构造和析构函数
    
    
    }
    int main()
    {
    	//test1();
    	test2();
    	system("pause");
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29

    在这里插入图片描述

    我们可以发现编译器确实是自动调用了构造和析构函数;在创建p1的时候,编译器自动掉了构造函数,在离开test2时对象销毁,析构函数被自动调用;
    同时如果我们没有设计构造函数和析构函数的话,编译器提供的构造和析构函数,就像我写的函数那样,只不过编译器提供的没有cout输出语句;
    我们再来看看,在main函数里面创建给对象是不是这样呢?
    在这里插入图片描述
    我们可以发现,在main函数中似乎只调用了构造函数,析构好像没有被调用,这是为什么?难道是对象并没有被销毁?
    的确如此,我们代码在执行到system的时候就停了下来,也就是说我们这时候并没有离开main函数,自然对象也就没有被销毁,自然析构函数也就无法被调用:
    他不是提醒我们按任意键继续吗?按呗,按了就能结束main函数,也就能销毁对象了:
    在这里插入图片描述
    还有一种办法就是,我们自己手动调用析构函数,让其提前清理:
    在这里插入图片描述
    (为了方便演示稍微改了下Person的权限)但是我们无法手动调用构造函数,因为在创建对象的时候,编译器已经自动调用了,不需要我们去手动调用;当然我们也只能提前销毁对象里面的数据,并不能干扰对象的生命周期,在最后对象销毁的时候,编译器还是会自动调用析构函数来清理对象里面的数据:
    在这里插入图片描述
    同时我们需要注意下,就是我们设计的构造函数和析构函数一定要让编译器访问的到,不要设置为私有或者保护属性,不然编译器会报错!!!

    3、构造函数的分类

    由于析构函数不能发生重载技术,自然其类型也就只有哪一个,但是构造函数就有多个类型:具体可以按两方面来细分:

    1、按参数分:有参构造和无参构造,同时无参构造也被称为默认构造,编译器给我们提供的构造函数也就是无参构造;
    2、按类型分:普通构造构造函数和拷贝构造函数;
    
    • 1
    • 2

    4、构造函数的调用

    既然构造函数有这麽多种类,那么调用它的方式也是有三种:
    测试代码:

    class Person
    {
    public:
    	string m_name;
    	Person()
    	{
    		cout << "无参构造函数调用" << endl;
    	}
    	Person(string name)
    	{
    		m_name = name;
    		cout << "有参构造函数调用" << endl;
    	}
    	Person(const Person&p)
    	{
    		m_name = p.m_name;
    		cout << "拷贝构造函数调用" << endl;
    	}
    	~Person()
    	{
    		cout << "析构函数调用" << endl;
    	}
    };
    void test3()
    {
    	Person p1;
    }
    int main()
    {
    	test3();
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32

    解析一下拷贝函数的参数:const修饰主要是为了防止被拷贝的对象的数据被无意修改,加引用主要是为了节省空间,不然函数的形参会产生一共与之一模一样,大小一样的形参对象,有点浪费空间,故我们选择了引用;

    括号法

    括号法是怎么个用法呢?
    在这里插入图片描述
    当然看到这里,或许我们会疑惑如果是无参调用的话,既然无参嘛,那我们也用括号法,只是括号里面不写参数嘛 既将p1改写成:
    Person p1();可不可以呢?
    我们来运行看看:
    在这里插入图片描述
    我们可以编译器发出了警告:为什么?
    主要是编译器:会把Person p1();解释为一个函数声明,该函数声明为 函数名为p1,形参没有,返回值为Person类型;而不会将其解释为括号法调用的无参构造,
    前面刚开始我们不是说过,程序在调用对象时会自动调用构造函数(也就是我们创建对象的时候),无需自动调用,而且只会调用一次,(但是我们可以指定调用的构造函数的类型)这里括号法里面的括号,就好似再让我们选择调用那种类型的构造函数!!!

    显示法

    在这里插入图片描述
    在这里插入图片描述

    显示法的本质:在等号的右边是一个匿名对象,该对象没有名字,只有空间,如果是我们只看右边的话,是不是就是一个括号法的调用;现在我们再把等号左边加进来看看,是不是等号左边就相当于再为这个匿名结构体取名字,让这个匿名结构体完成有名的转换!!!但是如果对于一个匿名结构体来说没有没有给它取名字,而直接让其调用构造函数的话(相当于我们只有p2的右边部分的话,在这条语句执行完过后,该匿名对象就会被销毁):
    在这里插入图片描述
    我们可以发现输出语句是在调用析构函数之后才输出的,而调用析构函数的时候代表者对象的销毁,这说明在cout之前,该匿名对象就被销毁了;
    这点需要我们注意;
    还有一点我们需要注意:
    不要使用匿名构造函数,初始化匿名对象:
    在这里插入图片描述

    一运行起来我们发现,发生了错误,原因是重定义;主要是因为在编译器看来:
    Person (p2)与Person p2毫无差异,完全等价,相当于我们再一次定义了p2,所以会发生报错!!!

    隐式转换

    在这里插入图片描述

    隐式转换法的本质是显示法:
    在这里插入图片描述

    5、拷贝构造函数的调用时机

    1、一个相同类型的对象且已经赋值,作为右值赋给同类型的对象;
    在这里插入图片描述
    2、对象作为函数形参;
    在这里插入图片描述
    形参作为实参的临时拷贝自然也是将实参的所有数据一起拷贝过来,自然调用拷贝构造函数;
    3、对象作为函数返回值;
    在这里插入图片描述
    我们可以发现,在函数返回时调用了拷贝构造函数,我们能理解有参构造函数的调用,但是为什么会调用拷贝构造函数?
    主要是因为:我们return的并不是p1也不可能是p1!!我i们返回的是p1里面的数据,编译器呢,在return创建了一个相同类型的对象,也就是匿名对象,编译器将p1里面的数据全部拷贝到这个匿名对象里面去,自然也就会调用拷贝构造函数,我们return的时候也是return的这个匿名对象;
    注意事项:
    关于返回的匿名对的去留:
    1、如果返回值作为同类型对象的右初始值,那么这个匿名结构体变为有名;
    就像这样:
    在这里插入图片描述
    在这里插入图片描述

    该匿名结构体在函数调用完毕过后,不会被析构掉,转而变为名字叫做p2的有名对象;
    2、如果返回值作为同类型对象的右值,不是初值,则该匿名对象会将其里面数据拷贝进该同类型对象过后,随着该语句执行完,然后被析构掉:
    在这里插入图片描述
    3、单纯的只调用函数,该匿名结构体在该语句执行完过后,立马被销毁;
    在这里插入图片描述

    6、构造函数调用规则

    对于构造函数和析构函数来说,我们是必须调用的,如果没有设计,编译器会自动给我们提供,但却是空实现;
    对于构造函数来说编译器一般会给我我们提供2种构造函数:
    如果我们设计的类里面压根就没有设计构造和析构函数,一般情况下,编译器会自动给我们提供:无参构造函数、拷贝构造函数、析构函数;
    但是:
    1、如果我们只提供了无参构造函数,编译器依旧会提供拷贝构造函数和析构函数;
    2、如果我们只提供了有参构造函数,编译器不会提供无参构造函数,但会提供拷贝构造函数和析构函数
    3、如果我们只提供了拷贝构造函数,那么编译器不会提供无参构造函数,只会提供析构函数;
    我们依次来对3种情况进行测试:
    情况一:
    在这里插入图片描述
    在这里插入图片描述

    我们可以发现程序很轻松就跑过去:我们在没有设计拷贝构造函数的情况下,p2也得却成功拷贝了p1里面的数据
    情形二:
    在这里插入图片描述

    我们可以很明显的发现我们的p1发生了错误,没有默认构造函数!!!
    我们注释掉这代码,我们来看看拷贝构造函数:
    在这里插入图片描述
    我们可以发现实际符合预期;
    情形3:
    在这里插入图片描述
    我们可以发现实际是符合预期的;

    7、深拷贝与浅拷贝

    这个主要发生在拷贝构造函数里面:
    什么是浅拷贝。就是简单的拷贝:
    在这里插入图片描述
    举个例子:

    class Person
    {
    private:
    	int* Age;//利用堆区开辟空间
    	string m_name;
    public:
    	void setDate(string name,int age)
    	{
    		m_name = name;
    		Age = new int(age);
    	}
    	int getAge()
    	{
    		return *Age;
    	}
    	string getName()
    	{
    		return m_name;
    	}
    
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    现在我们什么构造和析构函数都没设计,我们用编译器提供的;
    在这里插入图片描述

    我们可以发现结果非常完美,的却如此,但是有一个小瑕疵,就是我们向堆区申请了空间,我们似乎忘记释放了,这可是个不好的习惯,我们改在何时释放?是不是对象销毁的时候释放最合适;
    那么我们可以在析构函数里面设计释放的这个操作:
    在这里插入图片描述
    我们接下来再来运行看看:
    在这里插入图片描述
    我们发现程序崩了,为什么????
    主要是因为浅拷贝存在的问题:我们前面说了,浅拷贝就是单纯的数据的拷贝,画图理解就是:
    在这里插入图片描述
    我们在对象销毁的的时候,按照栈区先进后出的原则,我们先对p2对象进行析构,那么我们是不是在这时候就会把我们从堆上开辟的空间给释放了,这不编译器不会报错;第二次我们再来对p1析构,同理我们p1里面的Age也存的是所开辟的空间的地址,但是我们之前在p2里面已经对这块空空间释放过了,我们现在又会对该空间进行释放,这不就造成,对同一块空间,进行多次释放吗?编译器自然会崩;
    那么我们应该如何解决该问题?
    这时候,这样编译器提供给我们拷贝函数的拷贝方式似乎不合理,我们需要重新设计一下拷贝方式,我们应该在堆区上重新开辟一块空间,我们可以这样设计:

    class Person
    {
    private:
    	int* Age;//利用堆区开辟空间
    	string m_name;
    public:
    	void setDate(string name,int age)
    	{
    		m_name = name;
    		Age = new int(age);
    	}
    	int getAge()
    	{
    		return *Age;
    	}
    	string getName()
    	{
    		return m_name;
    	}
    	Person()
    	{
    	}
    	Person(const Person& p)
    	{
    		m_name = p.m_name;
    		Age = new int(*p.Age);
    	}
    	~Person()
    	{
    		delete  Age;
    	}
    
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33

    我们再来运行一遍代码:
    在这里插入图片描述
    程序很愉快运行起来,没有刚才的报错;

    三、 初始化列表

    主要分为两种初始化方式:
    1、传统方式:
    在这里插入图片描述
    2、初始化列表:
    在这里插入图片描述

    静态成员

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

    ●静态成员变量

    。所有对象共享同一份数据

    。在编译阶段分配内存

    。类内声明,类外初始化

    ●静态成员函数

    。所有对象共享同一个函数

    。静态成员函数只能访问静态成员变量

    静态成员变量

    静态成员变量不属于任何一个具体的对象,但是所有对象都能对其进行修改和访问,在内存中只存在一份;
    在这里插入图片描述
    我们可以发现,我们无法正常编译,编译器告诉我们不认识??在访问静态变量前我们需要对其进行类外声明;
    比如:int Person::c=0;告诉编译器我们Person作用域下的静态变量,初始化为0;
    像这样设计过后我们就能正常访问了:

    在这里插入图片描述
    当然对于静态成员变量也是有访问权限的,如果你将其设计为私有权限,我们计算声明了也是访问不到的:
    在这里插入图片描述

    对于公共权限的静态成员变量我们有两种访问方式,上述是一种;
    还有一种就是:通过类名进行访问:
    在这里插入图片描述

    静态成员函数

    和静态成员变量一样:不属于任何一个具体对象,所有对象共享一份;
    与静态成员变量不一样,静态成员函数不需要声明,但是访问方式是一样的
    1、通过对象访问:
    在这里插入图片描述

    2、通过类名进行访问:
    在这里插入图片描述
    注意事项:
    1、静态成员函数,只能访问静态成员变量,不能访问非静态成员变量;应为我们静态成员变量在只有一份,我们可以很轻松的读取和写入,但是对于非静态成员变量,内存中,不同的对象,各有各的,静态成员函数,无法辨别所对应的成员变量是那个对象的,自然也就无法精确修改(主要是因为静态成员函数里面没有this指针),往大了说静态成员函数只能访问静态成员变量,其它非静态成员访问不到;但是非静态成员函数能访问静态成员,应为非静态成员函数里面有this指针
    在这里插入图片描述
    在这里插入图片描述

    四、 成员变量和成员函数分开存储

    我们先来计算一下,空类都大小:
    在这里插入图片描述
    我们可以发现结果是1,为什么?
    主要是因为,编译器为了区分不同的对象,特意划分的一块区域;比如:
    Person p1;和Person p2;是两个不同的对象,他们不可能使用同一块空间把,我们于是就给1块空间,意思意思,加以区分这是两个对象,不是一个;这一个字节,只起占位作用;
    在这里插入图片描述
    在这里插入图片描述
    不管我有多少个静态成员变量,我sizeof’算的都是非静态成员变量,算的是所有对象都有的;

    五、this指针

    每一个非静态成员函数只会诞生一份函数实例, 也就是说多个同类型的对象会共用一块代码那么问题是:这-块代码是如何区分那个对象调用自己的呢?

    C+ +通过提供特殊的对象指针, this指针,解决上述问题。this指针指向被调用的成员函数所属的对象

    this指针是隐含每一个非静态成员函数内的一种指针

    this指针不需要定义,直接使用即可,同时this指针不能被修改(指针不能变);
    this指针本质是:Person*const this;

    this指针的用途:

    ●当形参和成员变量同名时,可用this指针来区分

    ●在类的非静态成员函数中返回对象本身,可使用return *this
    用途1:
    在这里插入图片描述
    我们可以发现并没有赋值成功,主要是编译器将对象的a、b也判断成了形参的a、b,自然也就无法成功初始化;
    如何解决?
    this不是指向调用该函数的对象的指针吗,我们利用this指针来访问对象的成员变量就行了:
    在这里插入图片描述
    用途2:
    如果我们设计一个函数让a自增10:
    在这里插入图片描述
    如果我们还想加10,我们就在调一次
    多次加10,就多次调用;
    除了图中所写的我们还可以将fun1的返回值写成引用的形式:
    在这里插入图片描述
    形成链式访问;
    那我们可不可以将返回值设计成Person类型?
    在这里插入图片描述
    显然不行,为什么?
    我们前面说了对象最为返回值,函数返回的不是同一个对象,我们的链式访问都不是对同一个对象的成员函数进行访问,怎么肯自加呢?(翻看前面拷贝构造函数调用时机)
    我们还可以设计成指针:
    在这里插入图片描述
    当然this指针在静态成员函数中是没有的,这也是静态成员函数不能访问非静态成员变量的原因;

    六、 空指针访问成员函数

    在这里插入图片描述
    我们可以发现fun2很轻松访问到了;
    再来看看fun1:
    在这里插入图片描述

    我们发现代码崩了;
    为什么:
    主要是因为:
    在编译器看来:
    p->fun2()
    等价于:fun2();
    p->fun1()
    等价于:fun1();
    a+=10等价于this->a+=10;
    this=NULL;
    我们对空指针进行了解引用;坑=肯定不行;
    同理:Person A;
    A.fun2();//在编译器看来就等价于fun2();
    A.fun1();//在编译器看来就等价于fun1();

    七、const修饰成员函数

    常函数:

    ●成员函数后加const后我们称为这个函数为常函数

    ●常函数内不可以修改成员属性

    ●成员属性声明时加关键字mutable后,在常函数中依然可以修改

    常对象:

    ●声明对象前加const称该对象为常对象
    ●常对象只能调用常函数
    情况1:
    在这里插入图片描述
    我们可以发现,在加了const修饰过后,我们无法该表对象里面的成员变量;
    但是对于一些特殊的变量我们可以加mutable来取消const的影响:
    在这里插入图片描述
    情形2:
    被const修饰的对象,只能调用同样被const修饰的成员函数(常函数)为什么?
    const作用就是不让我们修改对象内部的数据,当然会限制任何方式来修改,包括通过成员函数,没有用const修饰过的函数,存在可以修改const修饰对象的嫌疑,而加了const修饰的成员函数不会,应为成员函数加const,保证了成员变量不会通过成员函数来修改:
    在这里插入图片描述

    本质上const不是修饰成员函数的,而是修饰this指针的
    既保证不能通过指针解引用的方式去改变:Person const * const this; (是前一个const)
    这也是为什么const修饰的对象只能访问常函数;

  • 相关阅读:
    神马自然排名如何提高?
    Flink Watermark机制
    什么是本地存储的有效期?
    王者荣耀-镜教学视频
    JS教程之 JavaScript 框架之战已经结束,而且只有一个赢家
    Linux 查找动态库位置
    CentOS基Docker容器时区配置解决方案
    Java IO: 使用 `FileInputStream` 和 `FileOutputStream` 进行文件操作
    C/C++内存管理(栈、堆区;malloc,new;内存泄漏等)
    如何在测试/线上环境页面访问本地接口?
  • 原文地址:https://blog.csdn.net/qq_62106937/article/details/126492149