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;
}

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

二、对象的初始化和清理

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

1、构造函数

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

2、析构函数

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

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

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;
}

在这里插入图片描述

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

3、构造函数的分类

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

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;
}

解析一下拷贝函数的参数: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;
	}

};

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

我们可以发现结果非常完美,的却如此,但是有一个小瑕疵,就是我们向堆区申请了空间,我们似乎忘记释放了,这可是个不好的习惯,我们改在何时释放?是不是对象销毁的时候释放最合适;
那么我们可以在析构函数里面设计释放的这个操作:
在这里插入图片描述
我们接下来再来运行看看:
在这里插入图片描述
我们发现程序崩了,为什么????
主要是因为浅拷贝存在的问题:我们前面说了,浅拷贝就是单纯的数据的拷贝,画图理解就是:
在这里插入图片描述
我们在对象销毁的的时候,按照栈区先进后出的原则,我们先对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、初始化列表:
在这里插入图片描述

静态成员

静态成员就是在成员变量和成员函数前加上关键字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修饰的对象只能访问常函数;

评论 28
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

南猿北者

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值