【 C++ 】类和对象的学习(三)

前言:

😘我的主页:OMGmyhair-CSDN博客

目录

一、初始化列表

二、类型转换

三、static成员

四、友元

五、内部类

六、匿名对象


一、初始化列表

当我们之前在写构造函数时,我们通常在构造函数内对成员变量进行赋值。但其实还有一种方法是对成员变量进行初始化,那就是初始化列表:

初始化列表的使用: 以⼀个冒号开始,接着是⼀个以逗号分隔的数据成
员列表,每个"成员变量"后⾯跟⼀个放在括号中的初始值或表达式。
但是函数体内也能对成员变量进行赋值,那么为什么还要初始化列表呢?
其实初始化列表可以认为是每个成员变量定义初始化的地方。
通过这句话我们来看看以下初始化需要注意的地方:
1.每个成员变量在初始化列表只能出现一次
其实很好理解,既然它是成员变量定义的地方,而成员变量不可以重复定义,因此也不能重复出现。
可以看到编译器对重复出现的成员变量进行了报错:已初始化。

2.引用成员变量,const成员变量,没有默认构造的类类型变量,必须放在初始化列表位置进行初始化。

这个也很好理解。像这三种变量,它们本身必须在定义时就进行初始化。而初始化列表就是它们定义的地方,所以它们在初始化列表中必须进行初始化:

(1)引用变量

对于未定义时未初始化的引用变量,编译器要求对齐进行初始化。

(2)const成员变量

至于被const修饰的的变量,定义初始化后就不能再改变,也就不能再赋值,所以必须在定义时就进行初始化。

(3)没有默认构造的类类型变量

如果一个类没有默认构造,那么它的对象在定义时就必须进行初始化,否则没有合适的构造函数供它使用。

对这三类变量正确的初始化可以如下:

class A
{
public:
	A(int x)
	{
		_a = x;
	}

private:
	int _a;
};

class Date
{
public:
	Date(int& y,int date=20140101)
		: year(y),
		  _date(date),
		  a(2014)
	{

	}
private:
	int& year;
	const int _date;
	A a;
};

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

首先来看下面的一段代码:

class Date
{
public:
	Date(int year = 2014, int month = 12, int day = 3)
		: _year(),
		  _day(13)
	{

	}

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

private:
	int _year = 1000;
	int _month = 10;
	int _day = 10;
	int _date;
};

int main()
{
	Date d1(2024, 9, 6);
	Date d2;
	cout << "d1日期为:";
	d1.Print();
	cout << "d2日期为:";
	d2.Print();
	return 0;
}

来看看结果和你想的是否一样:

(1).首先,我们要区分构造函数参数列表的缺省值和成员变量声明处的缺省值

参数列表的缺省值是给你在初始化对象时调用构造函数是否需要传参用的,而成员变量声明处的缺省值,是用来给没有初始化的成员变量用的。

如果你参数列表的值没有赋值给成员变量,那么成员变量的值不会受到改变,这就是为什么d1初始化时是(2024,9,6),但是d1和d2的实际结果相同。

(2).为什么_year的值是0而不是1000?为什么_date是随机值?

在C++其实int也有构造函数,在初始化列表中,我们显式写了_year(),这一过程编译器会调用int的默认构造函数将_year赋值为0。

而_date我们没有在初始化列表中显式写出,则不会调用构造函数,那么就是随机值。

(3)._month为什么是10?_day为什么是13而不是10。

在初始化列表中,我们没有显式写出_month,这时如果在声明中有缺省值,就会用缺省值对_month进行赋值。

而_day在初始化列表中被我们显式地写出,进行了初始化,就不会再去使用在声明中的缺省值。

4.对于没有显式在初始化列表初始化的自定义成员变量,编译器会调用它们自己的默认构造函数。如果没有默认构造函数,编译器会报错。

无论我们是否显式地写出初始化列表,每个构造函数都有初始化。无论我们是否在初始化中显式写出某个成员变量,每个成员变量都会走一遍初始化列表。

总结一下成员变量走初始化列表的各种情况:

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

我们用一道题目来加深理解:

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的值赋给_a2,但这个时候_a1还未进行初始化,因此_a1的值是随机值,所以_a2也是随机值。

第二个进行初始化的值是_a1,将a的值赋给_a1,而a的值为1,所以_a1的值也为1。

所以这道题的答案为1和随机值,选D。


二、类型转换

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

在上面的代码中就发生了隐式类型转换。在A a=2这句代码中产生了一个A类型的临时变量,2赋值给这个临时变量,这个临时变量再赋值给a。换句话说,2构造了一个A类型的临时对象,这个临时对象拷贝构造给a,而编译器遇到连续构造加上拷贝构造会直接优化成直接构造,意思是直接对a进行构造不用临时对象。

我们还能对这个临时对象进行引用:

因为临时对象具有常性,而我们引用的就是临时对象,所以需要加上const。

在C++11后我们还能进行多个参数的类型转换:

2.我们还能进行不同类类型对象之间的转换,需要相应的构造函数支持

class B
{
};

class A
{
public:
	A(int x)
	{
		_a = x;
	}

	A(int x, int y)
	{
		_a = x;
		_aa = y;
	}

	A(B b)//隐式转换需要相应的构造函数支持
	{
		_b = b;
	}
private:
	int _a;
	int _aa;
	B _b;
};

int main()
{
	A a = { 1,1 };

	//不同类类型对象之间的隐式转换
	B ab;
	A aa = ab;
	return 0;
}

3.在构造函数前面加explicit就不再支持隐式转换:

可以看到加入了explicit关键字后,编译器对隐式转换进行了报错。


三、static成员

静态成员就是静态成员变量和静态成员函数,首先来看看它们共同的特性:

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

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

class A
{
public:
	static int GetX()
	{
		return _ax;
	}

	static int _az;

private:
	static int _ax;
	static int _ay;
};

int A::_ax=1;
int A::_ay=2;
int A::_az=3;

int main()
{
	A a;
	cout << A::_az << endl;
	cout << A::GetX() << endl;
	cout << a._az << endl;
	cout << a.GetX << endl;
}

来看在类中静态成员变量的几个特性:

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

class Date
{
private:
	static int _year;
	static int _month;
	static int _day;
};

int Date::_year;
int Date::_month;
int Date::_day;
2.静态成员变量为所有类对象所共享,不属于某个具体的对象,不存在对象中,存放在静态区。
可以看到在ra的大小比rb的大小小4个字节,因为A类型的对象没有将静态成员变量a的大小算进去。
3. 静态成员变量不能在声明位置给缺省值初始化,因为缺省值是给构造函数初始化列表的,静态成员 变量不属于某个对象,不走构造函数初始化列表。
可以看到编译器对拥有缺省值的静态成员变量进行了报错:
再来看 静态成员函数的特性:
1.⽤static修饰的成员函数,称之为静态成员函数,静态成员函数没有this指针。因为它没有this指针,所以静态成员函数可以访问其它的静态成员,但不能访问非静态的。
2.非静态的成员函数,可以访问任意的静态成员变量和静态成员函数。

使用静态成员的特性,我们来看三道题:

1.实现一个类,计算程序中创建出了多少的类对象

class Create
{
public:
	Create()//默认构造函数
	{
		_count++;
	}

	Create(const Create& c)//拷贝构造函数
	{
		_count++;
	}

	~Create()
	{
		_count--;
	}

	static int GetCount()
	{
		return _count;
	}

private:
	static int _count;
};

int Create::_count=0;

2.求1+2+3+...+n,要求不能使用乘除法、for、while、if、else、switch、case等关键字及条件判断语句(A?B:C)。

class Sum
{
public:
    Sum()
    {
        _ret += _n;
        _n++;
    }

    static int GetRet()
    {
        return _ret;
    }

private:
    static int _n;
    static int _ret;
};

int Sum::_n = 1;
int Sum::_ret = 0;

class Solution
{
public:
    int Sum_Solution(int n) {
        //调用n个构造函数
        Sum* p = new Sum[n];
        delete[] p;//释放申请空间
        return Sum::GetRet();
    }
};

int main()
{

    cout << Solution().Sum_Solution(5) << endl;//使用匿名对象进行访问
    return 0;
}

3.设已经有A,B,C,D 4个类的定义,程序中A,B,C,D构造函数调用顺序为?()

设已经有A,B,C,D 4个类的定义,程序中A,B,C,D析构函数调用顺序为?()

C c;
int main()
{
A a;
B b;
static D d;
return 0;
}

选项:

A D B A C
B B A D C
C C D B A
D A B D C
E C A B D
F C D A B
首先看构造函数调用顺序,全局变量是在main函数之前就创建好的,至于局部静态变量是第一次运行到static D d才会初始化,那么调用顺序就非常清楚了,首先是全局变量C再是main函数中的A、B,直到运行到局部静态变量处才是D,故选择E选项。
再来看析构函数调用顺序,我们已知对象是后定义的先析构。局部静态变量的生命周期是全局的,而局部变量生命周期是在函数体内,肯定比全局和局部静态先一步销毁,所以是B、A这俩个先销毁调用析构。再来看全局和静态,它们的生命周期都是全局,所以谁后定义先析构,因此这俩个析构顺序是D、C,故选择B选项。

四、友元

友元提供了⼀种突破类访问限定符封装的方式,有时提供了便利。但是友元会增加耦合度,破坏了封装,所以友元不宜多用。友元分为:友元函数和友元类,在函数声明或者类声明的前面加friend,并且把友元声明放到⼀个类的里面。
首先来看友元函数的特性:
1.外部友元函数可访问类的私有和保护成员,友元函数仅仅是⼀种声明,他不是类的成员函数。
2.友元函数可以在类定义的任何地方声明,不受类访问限定符限制。
3.⼀个函数可以是多个类的友元函数。
4. 友元函数不能用const修饰,因为友元没有this指针
class Time;//Time的前置声明,否则Date不认识Time
class Date
{
	friend void Print(const Date& date, const Time& time);

public:
	Date(int year = 2014, int month = 12, int day = 13)
	{
		_year = year;
		_month = month;
		_day = day;
	}


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

class Time
{
	friend void Print(const Date& date, const Time& time);
public:
	Time(int hour = 21, int minute = 18, int second = 13)
	{
		_hour = hour;
		_minute = minute;
		_second = second;
	}

private:
	int _hour;
	int _minute;
	int _second;
};

void Print(const Date& date, const Time& time)
{
	cout << date._year << endl;
	cout << time._hour << endl;
}

int main()
{
	Date a;
	Time b;
	Print(a,b);
	return 0;
}

再来看看友元类的特性:

1.友元类中的成员函数都可以是另⼀个类的友元函数,都可以访问另⼀个类中的私有和保护成员。

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

3.友元类关系不能传递,如果A是B的友元, B是C的友元,但是A不是C的友元。

class Time
{
	friend class Date;
public:
	Time(int time=2129)
	{
		_time = time;
	}

private:
	int _time;
};

class Date
{
	friend class Age;
public:
	Date(int date=20240906)
	{
		_date = date;
		cout << _t._time << endl;//Date是Time的朋友,可以访问Time的私有
	}

private:
	int _date;
	Time _t;
};

class Age
{
public:
	Age(int age = 20)
	{
		_age = age;
		cout << _d._date << endl;//Age是Date的朋友,可以访问Date的私有
	}

private:
	int _age;
	Date _d;
};

但是这里的朋友是单向的,例如Date是Time的朋友,可以访问Time的私有。但是Time不是Date的朋友,不能访问Date的私有。

朋友关系不能连续。Date是Time的朋友,Age是Date的朋友,但是Age不是Time的朋友。


五、内部类

1.如果⼀个类定义在另⼀个类的内部,这个内部类就叫做内部类。内部类是⼀个独立的类,跟定义在全局相比,他只是受外部类类域限制和访问限定符限制,所以外部类定义的对象中不包含内部类。所以当我们计算外部类的大小时,不需要算上内部类
在下面示例中A类对象大小和C类对象相同,所以计算外部类大小时不需要算上内部类。
2.内部类默认是外部类的友元类。同样这种关系只是单向,内部类可以访问外部类的私有,但是外部类不能访问内部类的成员变量。
3.内部类本质也是⼀种封装,当A类跟B类紧密关联,A类实现出来主要就是给B类使用,那么可以考 虑把A类设计为B的内部类,如果放到private/protected位置,那么A类就是B类的专属内部类,其他地方都用不了。
像之前题目中的用类计算一个数的累和,可以将其改造成外部类和内部类。
class Solution
{
public:

    class Sum
    {
    public:
        Sum()
        {
            _ret += _n;
            _n++;
        }
    };

    int Sum_Solution(int n) {
        //调用n个构造函数
        Sum* p = new Sum[n];
        delete[]p;
        return _ret;
    }

private:
    static int _n;
    static int _ret;
};

int Solution::_n=1;
int Solution::_ret=0;

六、匿名对象

1.用类型(实参)定义出来的对象叫做匿名对象,相比之前我们定义的类型对象名(实参)定义出来的叫有名对象 。
2.匿名对象生命周期只在当前一行,⼀般临时定义⼀个对象当前用一下即可,就可以定义匿名对象。
当我们需要用到类里面的成员函数又不想初始化对象,就可以使用匿名对象来调用类里面的成员函数:
注意不要将匿名对象写成下面这样:
因为编译器无法识别是对象定义还是函数声明。

七、编译器优化

现代编译器为了追求效率会在不影响正确性的前提下对传值或传参过程中产生的拷贝进行省略。大部分编译器会对一个表达式中的连续拷贝进行优化,还有一些编译器更加“激进”,会对一些跨行跨表达式进行优化。

在下面这条语句中就产生了连续拷贝:

实际过程:

编译器优化后:


我现在使用的是vs2022,所以在一些场景下,优化比较厉害(以下都是在debug版本下进行):

场景1:

真实情况:

其实在这一过程中相当于省掉了a这一对象,因为a的析构是在打印函数之前,这里相当于直接构造临时对象。


场景2:

即使我们对a进行了前置++操作,编译器还是省掉了构造a的这一过程,可以看出编译器还是很厉害的。


场景3:

从这个场景来看,优化得就很明显了。不仅省掉了传值返回过程中临时对象的构造,还省掉了对a的构造,相当于直接用1去构造对象ret。



如果这篇文章有帮助到你,请留下您珍贵的点赞、收藏+评论,这对于我将是莫大的鼓励!学海无涯,共勉!😘😊😗💕💕😗😊😘




评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值