类和对象(下)

ʕ • ᴥ • ʔ

づ♡ど

🎉 欢迎点赞支持🎉

个人主页: 励志不掉头发的内向程序员

专栏主页: C++语言


文章目录

前言

一、再谈构造函数

二、类型转换

三、static成员

四、友元

五、内部类

六、匿名对象

总结


前言

我们本章节就来试着了解了解我们类和对象的最后一点内容,本章节也很重要,大家也要认真学习哦。


一、再谈构造函数

我们之前说了构造函数,但是为了降低难度,所以就把我们的构造函数分成2次来说明,这是构造函数的第二部分,也就是初始化列表。

我们来看看它的特征:

1、之前我们实现构造函数时,初始化成员变量主要是在函数体内赋值,构造函数初始化还有⼀种方式,就是初始化列表,初始化列表的使用方式是以⼀个冒号开始,接着是⼀个以逗号分隔的数据成员列表,每个"成员变量"后面跟一个放在括号中的初始值或表达式

2、每个成员变量在初始化列表中只能出现一次,语法理解上初始化列表可以认为是每个成员变量定义初始化的地方

3、引用成员变量,const成员变量,没有默认构造的类类型变量,必须放在初始化列表位置进行初始化,否则会编译报错

4、C++11支持在成员变量声明的位置给缺省值,这个缺省值主要是给没有显示在初始化列表初始化的成员使用的

5、尽量使用初始化列表初始化,因为那些你不在初始化列表初始化的成员也会走初始化列表,如果这个成员在声明位置给了缺省值,初始化列表会用这个缺省值初始化。如果你没有给缺省值,对于没有显示在初始化列表初始化的内置类型成员是否初始化取决于编译器,C++并没有规定。对于没有显示在初始化列表初始化的自定义类型成员会调用这个成员类型的默认构造函数,如果没有默认构造会编译错误

6、初始化列表中按照成员变量在类中声明顺序进行初始化,跟成员在初始化列表出现的的先后顺序无关。建议声明顺序和初始化列表顺序保持⼀致

我们的初始化列表的作用也是用来初始化我们的成员变量的,我们的类对象在初始化时首先调用的就是初始化列表,在去看构造函数。它的大概形态是这样

cpp 复制代码
Date(int year = 1, int month = 1, int day = 1)
	: _year(year), // 初始值
	_month(month), // 初始值
	_day(day + 1)  // 表达式
	_ptr(malloc(/*....*/)) // 表达式
{ }

我们的初始化列表在函数下面{}上面以一个:开始,再加上我们要初始化的成员变量,成员变量的后面()中就是我们要初始化的初始值或者表达式,如果有多个要初始化的对象,我们可以用,号进行连接。最后一个不用加,号。这就是我们初始化列表的样子。只要是这样写就可以了,格式什么样都行,可以就写一行,也可以像我这样分成4行来写。

cpp 复制代码
class Date
{
public:
	Date(int year, int month, int day)
		: _year(year),
		  _month(month),
		  _day(day)
	{

	}

	void Print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}

private:
	int _year;
	int _month;
	int _day;
};

int main()
{
	Date d1(2025, 8, 3);
	d1.Print();
	return 0;
}

它和我们的构造函数就目前看来是一模一样的。

同时我们的每个成员变量只能在初始化列表初始化一次,再次出现会报错。

我们的初始化列表是我们成员定义的地方,所以我们的初始化列表只能出现一次。我们之前说的我们的构造函数内部也可以初始化,但是不是我们成员变量定义的地方,函数内部只能给我们的成员变量来赋上我们想要的值来初始化,但是不是我们成员变量创建出来就该如此,只是达到了相同的效果,注意区分。

cpp 复制代码
// 初始化列表
int a = 1;

// 构造函数内部初始化
int a;
a = 1;

同时,有一些成员只能在初始化列表初始化,就比如const成员,引用和没有默认构造的类类型变量。原因看上面的代码就很好理解了,因为这些变量在创建后就没有办法赋值,只能是它创建出来是什么就是什么,所以想要给他们初始化就必须在它定义的地方给它们初始化而不能是在函数内部给它们重新赋值的方式。

我们可以看到我们在初始化列表就可以初始化,但是在函数内部就无法初始化(赋值)了。

cpp 复制代码
class Time
{
public:
	Time(int hour = 1)
		: _hour(hour)
	{
		cout << "Time()" << endl;
	}

private:
	int _hour;
};

当我们的类对象有默认构造时,我们初不初始化都无所谓

因为编译器会自动去调用Time的构造函数去给它初始化,但是如果没有默认构造时

cpp 复制代码
class Time
{
public:
	Time(int hour)
		: _hour(hour)
	{
		cout << "Time()" << endl;
	}

private:
	int _hour;
};

我们就必须也只能在初始化列表初始化了

我们C++标准没有规定一定要在初始化列表初始化我们的成员变量,但是如果我们有成员变量没有初始化的话,我们就不知道它们的具体值是多少,相当于是给我们埋了一个坑。

当然我们可能还看过这种写法

cpp 复制代码
class Date
{
public:
	Date(int year, int month, int day)
		: _year(year),
		  _month(month),
		  _day(day)
	{ }

	void Print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}

private:
	int _year = 1;
	int _month = 1;
	int _day = 1;
};

这是我们C++11之后打的一个补丁,这个相当于是在声明的时候提供一个缺省值,在我们没有在初始化列表进行初始化的成员变量如果有缺省值那就会初始化成缺省值。我们试着不给我们的_day初始化看看。

我们的成员变量走初始化列表的逻辑是这样的

我们初始化列表初始化的顺序是看我们声明成员变量时的顺序而非我们定义时的顺序

cpp 复制代码
class A
{
public:
	A(int a, int b, int c, int d)
		: _a(a),
		  _b(b),
		  _c(c),
		  _d(d)
	{
		cout << _a << _b << _c << _d;
	}
private:
	int _d;
	int _c;
	int _b;
	int _a;
};

我们这个初始化的顺序是a、b、c、d、还是d、c、b、a呢,我们调试看看

答案显而易见了,那就是我们的后者,所以初始化列表的顺序适合我们的声明有关的,我们尽量不要让声明和定义不一样为好。

我们在构造类时尽量使用初始化列表,因为无论如何我们的编译器都会先走初始化列表的。

二、类型转换

在我们的C++中支持内置类型转换成类类型的类型转换,但是需要满足一定的条件,我们来一起看看。

1、C++支持内置类型隐式类型转换为类类型对象,但是需要有相关内置类型为参数的构造函数

2、构造函数前面加explicit就不再支持隐式类型转换

3、 类类型的对象之间也可以隐式转换,需要相应的构造函数支持

我们先来看看这个类

cpp 复制代码
class A
{
public:
	A(int a1)
		: _a1(a1)
	{ }

	A(int a1, int a2)
		: _a1(a1),
		  _a2(a2)
	{ }

	void Print()
	{
		cout << _a1 << " " << _a2 << endl;
	}

	int Get() const
	{
		return _a1 + _a2;
	}


private:
	int _a1;
	int _a2;
};

我们可以看到我们的这个类有两个构造函数,此时我们的主函数可以试着用内置类型来初始化一下这个类

cpp 复制代码
int main()
{
	A aa1 = 1;
	aa1.Print();

	const A& aa2 = 1;
	return 0;
}

我们可以看到,我们是用一个整型去初始化这个类的,我此时运行一下可以看到

它确实初始化了,用的是我们的第一个构造函数。它本质就是隐式类型转换,我们的编译器先用我们的1构造一个A的临时变量,靠的是我们第1个构造函数,然后再用这个临时对象拷贝构造aa1,所以就达到了用整型去初始化类的效果。但是不是任意内置类型都可以这样的,主要还是看构造函数支持什么类型,才可以用什么类型,不然编译器会报错的。当然,如果是多参数的话在C++11之前是不能转的,但是在之后我们可以用这种办法转

cpp 复制代码
int main()
{
	A aa3 = { 3, 4 };
	aa3.Print();

	return 0;
}

这里我们调用的是第2个构造函数了。

当然,如果我们不想要我们的隐式类型转换发生,我们可以调用explicit关键字在构造函数前,就能避免

cpp 复制代码
class A
{
public:
	explicit A(int a1)
		: _a1(a1)
	{ }

	A(int a1, int a2)
		: _a1(a1),
		  _a2(a2)
	{ }

	void Print()
	{
		cout << _a1 << " " << _a2 << endl;
	}

	int Get() const
	{
		return _a1 + _a2;
	}


private:
	int _a1;
	int _a2;
};

这样就无法隐式类型转换了。

三、static成员

1、用static修饰的成员变量,称之为静态成员变量,静态成员变量⼀定要在类外进行初始化

2、静态成员变量为所有类对象所共享,不属于某个具体的对象,不存在对象中,存放在静态区

3、用static修饰的成员函数,称之为静态成员函数,静态成员函数没有this指针

4、静态成员函数中可以访问其他的静态成员,但是不能访问非静态的,因为没有this指针

5、非静态的成员函数,可以访问任意的静态成员变量和静态成员函数

6、突破类域就可以访问静态成员,可以通过类名::静态成员或者对象.静态成员来访问静态成员变量和静态成员函数

7、静态成员也是类的成员,受public、protected、private 访问限定符的限制

8、静态成员变量不能在声明位置给缺省值初始化,因为缺省值是构造函数初始化列表的,静态成员变量不属于某个对象,不走构造函数初始化列表

我们可以给我们的类内声明一个静态成员变量

cpp 复制代码
class A
{
public:
	A()
	{}
private:
	// 静态成员变量
	static int _scout;
};

但是我们却不能在类内去初始化和定义我们的静态成员变量,因为我们的静态成员变量不会走我们的初始化列表,它存储在我们的静态区,它不是存储在我们对象里面的。我们可以看看我们的A类对象的大小

我们可以看到我们的类并没有存储我们的静态成员变量。如果我们想要初始化的话我们得在类外初始化才可以。

cpp 复制代码
int A::_scout = 0;

这样才能成功的初始化我们的静态成员变量。其实我们理解这个静态成员函数可以认为它是静态全局变量,但是受到类域的限制。所以它的生命周期是全局的。所以它只能在类内使用,如果想要在类外使用得用域作用限定符突破类的限制就可以使用了。

当然,因为是在类内,所以也受访问限定符约束,所以只有公有才可以这样在类外使用。

我们用static修饰的成员变量是静态成员变量,那我们用static修饰的函数就是静态成员函数了,静态成员函数和成员函数不同,它没有隐藏的this指针,这导致它没有办法使用类内的成员变量而只能使用静态成员变量。

cpp 复制代码
int main()
{
	cout << A::GetACount() << endl;

	A a1;
	cout << a1.GetACount() << endl;
	return 0;
}

调用方式和成员函数一样。

因为没有this指针所以没办法调用成员函数和成员函数(调用成员函数得要有一个隐藏的this指针,但是静态成员函数没有,所以静态成员函数不能调用成员函数)。

但是我们的成员函数可以访问我们的静态成员变量,因为就在类内,所以连突破类域都省了。

四、友元

我们之前就有说过友元函数了,但是那里只是浅浅的说明了怎么使用,这里我们再来说说它的特征吧。

1、友元提供了一种突破类访问限定符封装的方式,友元分为:友元函数和友元类,在函数声明或者类声明的前面加friend,并且把友元声明放到一个类的里面

2、外部友元函数可访问类的私有和保护成员,友元函数仅仅是一种声明,他不是类的成员函数

3、友元函数可以在类定义的任何地方声明,不受类访问限定符限制

4、一个函数可以是多个类的友元函数

5、友元类中的成员函数都可以是另一个类的友元函数,都可以访问另一个类中的私有和保护成员

6、友元类的关系是单向的,不具有交换性,比如A类是B类的友元,但是B类不是A类的友元

7、友元类关系不能传递,如果A是B的友元, B是C的友元,但是A不是C的友元

8、有时提供了便利。但是友元会增加耦合度,破坏了封装,所以友元不宜多用

我们前面说了我们的友元是在突破我们类域的访问限定符时使用的,这就是友元的作用。我们的友元分为友元函数和友元类,友元函数我们之前就见过了,就是函数声明前面加friend

cpp 复制代码
class A
{
	friend void func(int a, int b);
};

void func(int a, int b)
{ }

这样我们的函数func就能访问我们类A的私有成员了。我们的友元函数仅仅是一种声明不是我们类的成员函数,大家不要混淆,而且我们的友元函数可以在任意的地方声明,不受访问限定符限制,因为它只是声明。

我们的友元函数没有次数上限,也就是说我们一个函数可以是很多个类的友元。

cpp 复制代码
class A
{
	friend void func(int a, int b);
};

class B
{
	friend void func(int a, int b);
};

class C
{
	friend void func(int a, int b);
};

//.......

void func(int a, int b)
{ }

第二种友元是友元类,我们的友元类中的成员函数都是我们第二个类的友元函数,都可以访问友元类的私有或保护的成员。

cpp 复制代码
class A
{
	// 友元类
	friend class B;
public:

private:
	int _a1 = 1;
	int _b1 = 2;
};

class B
{
public:
	void func(A a)
	{
		cout << a._a1 << " " << a._b1 << endl;
	}
};

int main()
{
	A a;
	B b;
	b.func(a);

	return 0;
}

我们的B可以访问我们A的私有成员。

我们的友元关系是单向的,不是说A是B的友元,那B就是A的友元了,也就是说,B可以访问A的私有,但是A还是不能访问B的私有。而且也没有传递关系,也就是说A是B的友元,B是C的友元,但是C和A直接没有任何关系,也没有谁是谁友元的说法。

五、内部类

1、如果一个类定义在另一个类的内部,这个内部类就叫做内部类。内部类是一个独立的类,跟定义在全局相比,他只是受外部类类域限制和访问限定符限制,所以外部类定义的对象中不包含内部类

2、内部类默认是外部类的友元类

3、内部类本质也是一种封装,当A类跟B类紧密关联,A类实现出来主要就是给B类使用,那么可以考虑把A类设计为B的内部类,如果放到private/protected位置,那么A类就是B类的专属内部类,其他地方都用不了

所谓的内部类就是把一个类定义在另一个类里面的操作

cpp 复制代码
class A
{
private:
	static int _k;
	int _h = 1;
public:
	class B
	{
	public:
		void foo(const A& a)
		{
			cout << _k << endl;
			cout << a._h << endl;
		}
    private:
        int _b = 1;
	};
};

int A::_k = 1;

因为B在A中,所以B就自动是A的友元类了。

cpp 复制代码
int main()
{
	cout << sizeof(A) << endl;
  
	return 0;
}

我们可以来先看看我们的类A的大小是多大

我们可以看到,我们的A的大小是4而非8,实际上我们的A中只有一个_h而没有_b。我们的内部类是一个独立的类,和我们的外部类相比只是受到了我们的外部类域和访问限定符的限制而已,所以我们的外部类是不包含我们的内部类的,所以我们看到我们的A的大小是4。

cpp 复制代码
int main()
{
	// 指定类域
	A::B b;
	A aa;
	b.foo(aa);
	return 0;
}

六、匿名对象

1、用类型(实参) 定义出来的对象叫做匿名对象,相比之前我们定义的类型对象名(实参) 定义出来的叫有名对象

2、匿名对象生命周期只在当前一行,一般临时定义一个对象当前用一下即可,就可以定义匿名对象

cpp 复制代码
class A
{
public:
	A(int a = 0)
		:_a(a)
	{
		cout << "A(int a)" << endl;
	}
	~A()
	{
		cout << "~A()" << endl;
	}
private:
	int _a;
};
cpp 复制代码
int main()
{
    // 有名对象
    A aa1;

    //A aa1(); 不能这么定义,因为分不清是函数声明还是定义
    
    //匿名函数
    A();
    return 0;
}

我们的匿名对象一般是懒着给函数起名字使用的

cpp 复制代码
class Solution {
public:
	int Sum_Solution(int n) {
		//...
		return n;
	}
};

int main()
{
	// 这是有名对象定义的方式
	Solution st;
	cout << st.Sum_Solution(10) << endl;

	//这是匿名对象
	cout << Solution().Sum_Solution(10) << endl;

	return 0;
}

这样会方便很多,当然也还有别的用法,以后碰到了再说吧。


总结

以上便是我们C++的类和对象的全部内容啦。学到这里,我们的C++就已经初窥门径了,也可以领略到我们C++的和C语言不同的地方了,下一章节将会说明我们C++中创建空间的不同方式,加油。

🎇坚持到这里已经很厉害啦,辛苦啦🎇

ʕ • ᴥ • ʔ

づ♡ど

相关推荐
Jooolin4 分钟前
【教你一招】反汇编有啥用?
c++·ai编程·汇编语言
liulanba12 分钟前
NAT 和 PNAT
开发语言·php
hqwest1 小时前
C#WPF实战出真汁01--项目介绍
开发语言·c#·wpf
xnglan2 小时前
蓝桥杯手算题和杂题简易做法
数据结构·数据库·c++·python·算法·职场和发展·蓝桥杯
melonbo2 小时前
代理模式C++
c++·设计模式·系统安全·代理模式
姓刘的哦2 小时前
Win10上Qt使用Libcurl库
开发语言·qt
檀越剑指大厂2 小时前
【开发语言】Groovy语言:Java生态中的动态力量
java·开发语言
stbomei2 小时前
C 语言判断一个数是否是素数的三种方法文章提纲
c语言·开发语言·算法
小牛壮士2 小时前
Tokenizer(切词器)的不同实现算法
开发语言·算法·c#
小徐敲java2 小时前
python-pycharm切换python各种版本的环境与安装python各种版本的环境(pypi轮子下载)
开发语言·python·pycharm