前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >C嘎嘎入门篇:类和对象(3)

C嘎嘎入门篇:类和对象(3)

作者头像
用户11295429
发布2024-10-16 17:12:37
420
发布2024-10-16 17:12:37
举报
文章被收录于专栏:王的博客专栏

前言

小编在写完了类和对象的1,2以后,下面紧接着开始类和对象3的学习,这一部分的知识是很重要的,各位读者朋友一定要好好的理解这篇文章,现在,代码时刻到。

正文:

1.再探构造函数

前瞻

可能很多读者朋友看到这个标题会感到很奇怪,因为小编曾在类和对象(2)中就已经写了构造函数的用法,那个时候可能很多读者朋友就已经认为构造函数那样写就可以了,其实小编在刚学到这里的时候也是和你们一样的感受的,不过学完之后我才知道构造函数原来可以那么写,下面小编就不在多废话了,开始构造函数的进一步讲解,在讲解之前,可能有很多读者朋友会好奇为什么要学习这种构造函数的写法,小编就拿一个简单的例子进行举例:我们都晓得,成员变量可以使内置类型,也可以是自定义类型,对于自定义类型的成员变量,在进行初始化的时候会调用它的构造函数,但是如果出现下面小编代码所示的情况,此时我们已知的构造函数写法就不成立了:

代码语言:javascript
复制
class Time
{
	Time(int time)
	{
		_time = time;
	}
private:
	int _time;
};
class wang
{
public:
	wang(int year)
	{
		_year = year;
	}
private:
	int _year;
	Time a;  //此时我们不难发现,此时的time我们如果不给值的话是不可以进行初始化的,因为此时小编写的是带参的构造函数
};

此时这个代码是编译不过去的,系统会提示我们此时的Time类是没有默认构造函数的 :

所以此时我们再探构造函数中的初始化列表就是为了解决这种问题的,下面小编就开始再谈构造函数喽~

1.1.再探构造函数的特点

1.之前我们在实现构造函数的时候,初始化成员变量的时候主要在函数体内进行初始化操作的,构造函数还有一种写法,它就是我们今天的重点——初始化列表,初始化列表的使用方式是以冒号:开头,接着是以一个逗号为分割的数据成员列表,每个“成员变量”后面都会跟着一个放在括号的初始值和表达式,最后一定要记着要写函数体,里面可以不写内容,但是必须要有:{};这便是初始化列表的写法,下面小编先来展示一下它的代码是如何去书写的:

代码语言:javascript
复制
class Date
{
public:
	Date(int year = 2024, int month = 0, int day = 28)  //对于参数列表,还是和构造函数一样,我们可以写全缺省的(小编喜欢写),也可以写别的
		:_year(year)
		, _month(month)
		, _day(day)
	{}
private:
	int _year;
	int _month;
	int _day;
};

上面便就是对于一个初始化列表的写法,写起来也是蛮容易的,对于为什么会一个成员变量后面跟着一个括号,可以这么去理解,把括号想象成赋值运算符‘’=‘,就可以了,其实就是把形参一一的配对给成员函数,对于函数那个大括号的作用说白了就是为了补充用的,后面小编在讲string类的时候会再提到这个大括号的,对于初始化列表,小编的建议就是以后我们在写构造函数的时候要多会去使用初始化列表,小编讲到后面各位就知道我为何这么说了,各位读者朋友一定要对这方面有足够的认知,下面我们继续讲述构造函数的其余特点!

2.每个成员变量只可以在初始化列表出现一次,语法理解上初始化列表可以认为是每个成员变量定义初始化的地方。这个特点也是很容易理解的,因为成员变量的初始化已经在初始化列表进行初始化了,我们没必要在多写几个初始化,重复的初始化是没有用的,所以每个成员变量只可以在初始化列表出现一次,不可以多次出现。

3.引用成员变量,const成员变量,没有默认构造的类类型变量,必须放在初始化列表位置进行初始化,否则会编译报错。对于没有默认构造的类类型对象,小编在讲本小节之前就说过,对于没有默认构造的类类型对象的初始化,只可以在初始化列表进行初始化,否则是会编译报错的,对于为什么这么做,各位读者朋友可以不用深究,因为这是祖师爷进行规定的,咱们不用对此有太多的了解,知道这么个事就行,我们再说说其他两个,对于const类型的成员变量,我们知道const类型的变量必须一开始就要初始化,之后这个变量就不可以被修改了,对于此我们也需要从初始化列表进行初始化的,因为我们调用构造函数的时候会先走一遍初始化列表的,等会小编就会说构造函数进行构造的顺序,如果我们把const变量放到函数体内,这样的话还是会报错,如下图所示:

代码语言:javascript
复制
class wang
{
public:
	wang(int a = 12)
	{
		_a = a;
	}
private:
	const int _a;
};

写在函数体内是会报错的,所以我们需要把const类型的变量放置到初始化列表中,这个特点也需要记住,不然以后我们犯错可能都不知道错是在哪里进行出现的;对于引用成员变量,引用小编在前几篇博客就讲到过,我们需要知道引用类型的变量,我们一定要用一个被引用的对象,不然这样是会报错的,总不能会有引用空这个概念吧,所以对于引用的成员变量,我们也需要走初始化列表才可以初始化成功,不然依旧会编译出现错误,这个是硬性要求,各位读者朋友一定要记住这个特性,不要在以后犯下这个错误了。

4.C++11支持在成员变量声明的时候给缺省值,这个缺省值主要给没有显示在初始化列表初始化的成员使用的。对于这个特性,小编用自己的话给各位读者朋友解释一下,我们在声明处给了成员变量缺省值以后,当我们没有写初始化列表的时候,会直接使用缺省值,可以这么想,其实成员变量“”走了一遍初始化列表”,只是此时初始化列表的那些参数都是我们在声明处给的缺省值,这样记我觉着这个特性会很容易记住的。

5.我们需要尽量使用初始化列表进行初始化(挑重点,尽量),因为那些你不在初始化列表进行初始化的成员依旧会走遍一遍初始化列表(函数体也在初始化列表中),如果初始化列表没有进行初始化,那么会看一眼此时的声明是否有缺省值,如果有缺省值的话就按照缺省值来进行初始化,如果缺省值也没有的话,那么就考验编译器了。C++,对于自定义类型的成员变量会默认的去走它的默认构造,对于内置类型的话,看编译器会不会给它一个默认值,当然,如果自定义类型没有默认构造的话,会直接报错,例如上面那样,所以小编这里用一张图来帮助各位去了解构造函数对成员变量初始化的顺序:

6.初始化列表中是按照声明顺序进行初始化的,而不是按照初始化列表的顺序进行初始化的。这个特性各位读者朋友一定要记住,以后在做相关习题的时候,可能会有题会因为这个特性进行挖坑的,所以小编建议各位读者朋友的初始化列表声明和定义顺序保持一致。

1.2.初始化列表总结:

1.无论是否显示写初始化列表,每个构造函数都有初始化列表

2.无论是否在初始化列表显示初始化,每个成员都有走初始化列表进行初始化。

1.3.一个小题

下面程序的运行结果是什么()

A.输出1,1 B.输出2,2 C.编译报错 D.输出1,随机值 E.输出1,2 F.输出2,1

代码语言:javascript
复制
class A
{
public:
	A(int a)
		:_a1(a)
		, _a2(_a1)
	{}
	void Print()
	{
		cout << _a1 << "," << _a2 << endl;
	}
private:
	int _a2 = 2;
	int _a1 = 2;
};

int main()
{
	A aa(1);
	aa.Print();
}

各位读者朋友先思考一下这个题目,下面小编直接开始讲解,这个题目考验的其实就是小编在前文说的构造函数的最后一个特点,构造函数的构造顺序是由声明顺序进行初始化的,我们先看成员变量的声明,发现是先声明的是_a2,然后是_a1,并且他们都有缺省值,我们再往上看构造函数,发现二者都在初始化列表中,所以缺省值我们是用不上的了,所以此时就很自然的排除了B,之后我们先看_a2的定义,发现它是按照_a1的值进行初始化的,但此时_a1并没有初始化,所以会是随机值,然后在看_a1的初始化,是按照a进行初始化,此时的a是1,所以此时的_a1是1,_a2是随机值,所以很自然的就可以选出选项:D,下面小编给出运行截图:

各位读者一定要好好掌握这个特性,千万不要掉进坑里,下面我们进入下一小节的讲解。

2,类型转换

前瞻

类型转换想必各位读者朋友都不陌生,我们在C语言阶段曾多次接触到类型转换,有显示类型转换(强转)和隐式类型转换,当然我们常用的是后者,就比如可以把一个浮点型的数据隐式转换成整形之类的,对于前者,我们在使用动态内存函数的时候常常用到它,就比如我们想开辟一个int类型的空间:int* a = (int*)malloc(sizeof(int)),类似这样,这便是强转,可能此时狠毒读者朋友会有疑问,那么类和内置类型是否可以实现隐式类型转换呢?答案是肯定的,下面小编就开始讲述类型转换的特点。

2.1.类型转换的介绍和特点

1.C++支持内置类型隐式类型转换成类类型的对象,不过需要有相关内置类型为参数的构造函数。这个特点告诉了我们内置类型是可以转换成类类型的,只不过在他的中间需要有好几个步骤,因为这个转换的原理就是先把内置类型通过构造函数构造出一个类类型的临时对象,然后把这个临时对象拷贝构造给类类型,这就是为何此时内置类型可以转换成类类型,下面小编给出一段代码,并且通过图解的方式来给各位读者朋友给出最详细的解答:

代码语言:javascript
复制
class wang
{
public:
	wang(int a = 102)
		:_a(a)
	{
		cout << "wang(int a = 102)" << endl;  //通过打印来看看内置类型转换成类类型的效果
	}
	wang(const wang& x)
	{
		_a = x._a;
		cout << "wang(const wang& x)" << endl;
	}
private:
	int _a;
};

int main()
{
	wang s1 = 1;
	return 0;
}

此时是经历了两次构造,分别是正常的构造函数和拷贝构造函数,不过此时编译器并不会去调用两次构造函数来进行隐式类型转换,虽然原理是这样的,但是编译器是会自己优化自己的算法的,所以多次构造函数 + 拷贝构造函数会被编译器优化成直接构造,所以当我们运行这个代码的时候,仅仅只会构造函数,如下图所示:

所以对于这种隐式类型转换,我们需要知道它其中的原理,它是走了一次构造函数又加上一次拷贝构造,不过编译器是会自己去优化的,他是会优化成仅仅走了一次拷贝函数的,各位读者朋友千万不要仅仅去记住此时隐式类型转换的时候仅仅走了一次构造函数,而没有拷贝,编译器会随着时间的提升慢慢去优化的,有一些优化过程虽然会让我们在运行的时候减少内存的形成,同样的我们也会逐渐忘记底层的视线逻辑,我们一定要知晓其中的逻辑,这会让我们之后的代码生活会更愉快,行了废话不多说了,继续下两个特点的讲述:

2.构造函数前面加explicit就不会支持隐式类型转换了。可能有些情况我们并不想进行隐式类型转换,语法上同样给咱们提供了一个关键字让咱们可以让隐式类型转换实现不了,这就是explicit关键字, 如果加上它,编译器会出现下面这种情况:

3.类类型的对象之间也可以隐式转换,需要相应的构造函数支持。这个特性读者朋友们知道就可以,小编就不多举例了,下面进入下一个小节的讲解~

3.static成员

前瞻

static这个关键字想必各位读者朋友在C语言阶段都接触过,这个关键字的作用就是让一个局部变量变成静态的变量,简单来说就是把一个局部变量升级成全局变量,这个关键字同样也可以用到类里面,让类的成员变量变成静态的成员变量,它甚至可以用到类的成员函数上,让类的成员函数变成静态区的成员函数,预知为何如此,请看下面分解:

3.1.static成员的介绍和特点

1.用static修饰的成员变量被称之为静态成员变量,静态成员变量一定要在外部进行初始化。讲解这个之前,小编先上一段代码:

代码语言:javascript
复制
class wang
{
public:
	void Print()
	{
		cout << a << endl;
	}
private:
	static int a;
};
int wang::a = 12;

此时这个代码就说明了,对于static修饰的成员变量,想要初始化它,并不能在类里面进行初始化了,可以理解为它是静态区的,它已经跳脱出类了,仅仅是受到了类的限制而不能完全跳出来,意思就是如果我们想要使用静态成员变量,那么就需要通过域作用限定符来使用它们,初始化成员变量的时候就展示了这个特点,我们得说明这个a是在wang这个类中,然后才能给它初始化,各位读者朋友好好理解。

2.静态成员变量是所有类对象共享的,不属于某个具体的对象,不存在对象中,存在于静态取中。这句话的意思就是我们在实例化多个wang类型的对象,此时这个a并不是每个对象特有,它们都没有动它的权利,仅仅可以使用它罢了,所以这个静态成员变量是所有对象公有的,至于其他的文字小编在前面说过了我就不多说了。

3.用static修饰过的成员函数,被称之为静态成员函数,静态成员函数是没有this指针的。这个特点也是很好去理解的,因为此时被static修饰过的函数属于是静态区的,与这个类是没有什么关系的了,自然是没有this指针了,这个特点一定要了解,避免以后出大错,小小的细节掌握的越好,未来走的路就越长。

4.静态成员函数只可以访问其他的静态成员,但是不可以访问非静态的,因为没有this指针,这个特点也是比较容易理解的,小编就不多说明了。

5.非静态的成员函数,可以访问任意的静态成员变量和静态成员函数。这个也是很好理解的,因为静态成员变量是一个公共区,在类里面任何函数都可以去使用的。

6.静态成员也是类的成员,同样也是受到public,private,project的限制。

7.突破类域就可以访问静态成员,可以通过类名::静态成员或者是对象.成员变量来访问静态成员变量和静态成员函数,当然后者的前提是这两个静态的都在public内。

8.静态成员变量是不可以在声明位置给缺省值的,因为小编在前面也说过,缺省值实际上是给初始化列表进行使用的,静态成员变量不属于某个对象,所以用不了初始化列表,自然是给不了缺省值的。此时这便是static成员变量,下面进行一个小题的讲解。

各位读者朋友先看一下这个题目,下面小编直接进行讲解:这个题目是考验我们构造函数的使用顺序和析构函数的使用顺序,先看构造函数,这个其实是很简单的,构造函数其实是依次使用的,所以这个顺序也是蛮简单的,首先是先构造C,因为C是刚开始出现的,然后就进入了main函数里面,在main函数里面,首先构造的是A,然后是B,最后是D,所以顺序自然是C,A,B,D。这个选项还是比较容易的,析构函数就牵扯的多了,首先各位要知道我们肯定是要调用析构函数,因为此时是结束main函数,main函数里面生命周期结束的只有a,b,小编在析构函数的时候讲过,后定义的先析构,所以此时先析构B,然后析构A,之后就开始析构全局变量C,D,它们的析构也是按照这个特点来的,D是最后一个构造的,所以D先析构,然后C在析构,所以这个题目选E,B。大家一定要好好掌握。

4.友元

前瞻

友元函数其实我们是并不算太陌生的,小编在前面的时间类的书写中,就曾写过友元函数,当时是为了解决流提取函数和流插入函数的书写习惯才用的,友元函数可以看作是类的朋友,所以友元函数是可以使用类里面的所有东西的,下面开始讲述友元的更多特点的。

4.1.友元的介绍和特点

友元提供了一种突破类访问限定符封装的方式,友元分为:友元类和友元函数,在函数声明或者类声明的前面加friend,并且把友元声明放在一个类里面。这个详细讲述了友元的分类,并且告诉我们如何去设置一个友元函数和友元类,下面小编就通过代码给大家展示一下如何建立一个友元函数以及友元类:

代码语言:javascript
复制
class wang
{
public:
	//下面都是友元声明
	friend int getyear(const wang& s);
	friend class Time;
	wang(int year = 2024)
		:_year(year)
	{}
private:
	int _year;
};

int getyear(const wang& s)
{
	return s._year;
}

class Time
{
public:
	Time(int time = 2022)
		:_time(time)
	{}
	void Print(const wang& s1)
	{
		cout << s1._year << endl;   // 此时在Time类是可以去使用wang这个类里面任何东西的
	}
private:
	int _time;
};

2.外部友元函数是可以访问类的私有保护成员的,友元函数仅仅是一种声明,它并不是一个类的成员函数。 这个特性就说明了友元函数的立场,友元函数并不是一个类的成员函数,它仅仅就是类的一个朋友罢了。

3.友元函数是可以在任意位置进行声明的,它并不收到访问限定符的限制。这个特性和上面那个特性一起记会比较好,上面说了友元函数并不是类的成员函数,所以访问限定符限制不了它,不过小编还是推荐各位在类开头进行友元函数声明,这样会显示的好点。

4.一个函数可以是多个类的友元函数,可以理解为,一个人可以是很多个人的朋友。

5.友元类的成员函数都可以是另一个类的友元函数,都可以访问另一个类的私有保护成员。这个也是很容易理解的,也是属于友元的特性,读者朋友们记住就好。

6.友元类的关系是单向的,没有交换性,就比如上面的Time类是wang类的友元类,但是wang类却不是Time类的友元类。

7.友元类的关系是不可以传递的,就比如A类是B类的友元类,B是C的友元类,A就不可以是C的友元类,因为友元类是没有传递性的。

8.友元虽然会提供方便。但是友元多了会增加耦合度,破坏了封装,所以友元要少用,对于这个特性小编会在后期在继续说的,目前学的还没有那么深,我自认为我无法解释清楚。

5.内部类

5.1.内部类的介绍

如果一个类定义在了另一个类的内部,这个内部的类就被叫做内部类。内部类其实是一个独立的类,跟定义在全局的相比,他仅仅是收到了类类域的限制以及访问限定符的限制,所以外部类定义的对象中不包含内部类,内部类其实也是属于一个外部的类,只不过是一个被限制着的外部类,下面小编展示一下他的写法:

代码语言:javascript
复制
class Date
{
public:
	Date(int year = 2024, int month = 9, int day = 28)
		:_year(year)
		, _month(month)
		, _Day(day)
	{}
	class Time
	{
		Time(int time = 1010)
			:_time(time)
		{}
	private:
		int _time;
	};
private:
	int _year;
	int _month;
	int _Day;
};
5.2.内部类的特点

1.内部类默认为外部类的友元类。这个就和前面的知识联系起来了,因为内部类其实是属于外部的,不过就是此时的内部类收到外部类的限制罢了,所以内部类也是可以使用外部类的成员函数和成员变量的,这个点读者朋友一定要记住。

2.内部类本质是一种封装,当A类和B类关系紧密时,A类实现出来就是给B类使用的,那么可以考虑把A类设计为B的内部类,如果放到private/protected的位置,那么A类就是B类的专属内部类,其他地方都用不了,对于这个特性,小编会在别的博客中用到,那个时候会让读书朋友更深刻的理解到这个点。

这便就是内部类的相关知识点,其实知识点也是蛮少的,小编其实学到了后面也是不太常用内部类(可能是我太菜,学的很浅所以没用上),不过内部类对于解决一些习题是有妙用的,这个题目小编先给大家留个坑,以后我肯定会写的(但愿吧),下面进入本文最后一个知识点的讲解。

6.匿名对象

6.1.匿名对象的介绍和特点

1.用类型定义出来的变量叫做匿名对象,相比之前我们定义的类型对象名定义出来的对象叫做有名对象,下面小编展示一下匿名对象的写法:

代码语言:javascript
复制
class wang
{
public:
	wang(int year = 2024)
		:_a(year)
	{}
private:
	int _a;
};

int main()
{
	wang();
}

对于匿名对象,各位读者朋友一定要记住,匿名对象的建立一定记着加个括号,这样就代表着建立了一个匿名对象,如果没有括号的话,系统会分不清你到底是想写有名对象还是匿名对象,这个点要记住。

2.匿名对象的生命周期仅仅在当前·一行,一般临时定义一个对象当前用一下即可,就可以定义匿名对象。匿名对象看似没有什么用,其实用处也是很大的,就比如下面代码的用法:

代码语言:javascript
复制
void Func(const wang& s)
{

}

int main()
{
	//wang();
	Func(wang());
}

此时的匿名对象可以用作函数的实参,此时我们在使用完之后会自己析构,减少多余变量的创建,从而减小内存的使用,当然还有一些特殊场景,等小编以后遇到了会写文章进行补充的~对于匿名对象,其实如果我们加上static的话,会延长它的寿命,它的寿命会随着main函数程序的结束而结束,这一点各位读者朋友们记住就好。

总结

这篇文章到现在也结束了,同样也标志着类和对象的部分小编也写完了,其实本文小编还有很多的没有写,就比如编译器对于对象拷贝时的优化,小编认为自己这部分还没有完全掌握,功力不够,等小编掌握好这部分知识点以后在进行书写免得误人子弟,类和对象三部曲结束了,下面就要进入STL库的部分了,这部分的内容与类和对象的内容息息相关,读者朋友一定要掌握好,如果文章有错误,恳请在评论区指出,小编一定会及时改正,那么,我们下一篇文章见喽!

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2024-10-16,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1.再探构造函数
    • 前瞻
      • 1.1.再探构造函数的特点
        • 1.2.初始化列表总结:
          • 1.3.一个小题
          • 2,类型转换
            • 前瞻
              • 2.1.类型转换的介绍和特点
              • 3.static成员
                • 前瞻
                  • 3.1.static成员的介绍和特点
                  • 4.友元
                    • 前瞻
                      • 4.1.友元的介绍和特点
                      • 5.内部类
                        • 5.1.内部类的介绍
                          • 5.2.内部类的特点
                          • 6.匿名对象
                            • 6.1.匿名对象的介绍和特点
                            • 总结
                            领券
                            问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档