C++的魔法世界:类和对象的终章

一、再探构造函数

类和对象(中)里介绍的构造函数,使用的是赋值实现成员变量的初始化。而构造函数还有另一种初始化方法,就是使用初始化列表。

格式:

​ 成员变量以冒号开始,逗号分隔成员,成员变量后面跟上一个括号,其中放置一个初始值或表达式,函数体内实现格外的功能。

class Myqueue
{
public:
    Myqueue()
    :_pushst(10)
    ,_popst(10)
    {}
private:
	Stack pushst;
	Stack popst;
}

Myqueue的类编译器自动生成的构造函数调用了Stack的默认构造函数,完成初始化。栈没有默认构造函数,需要使用初始化列表实现自定义类型的默认构造函数。

必须使用初始化列表的类:

​ 没有默认构造的类类型成员变量、被const修饰的成员变量(声明时必须初始化)、被引用的成员变量(声明引用时必须初始化)

  • 自定义类型成员变量,没有默认构造想要调用它的构造需要传参,在函数体内无法实现,那就只能在初始化列表中实现传参。
#include <iostream>
using namespace std;
class Stack
{
public:
	Stack(int n)
		: _a(new int[n])
		, _top(0)
		, _capacity(n)
		{}
private:
	int* _a = new int;
	int _top = 0;
	int _capacity = 0;
};
class Myqueue
{
public:
	Myqueue()
		:pushst(10)
		,popst(10)
	{
		cout << "Myqueue()" << endl;
	}
private:
	Stack pushst;
	Stack popst;
};

栈没有实现默认构造函数,只实现了构造函数,在Myqueue中需要对两个栈类的成员变量进行初始化,需要调用它的默认构造而栈类没有实现,从而采用初始化列表。

在初始化列表中可以对这两个成员传参调用它们的构造函数从而实现初始化,而若是在函数体内实现初始化,是无法调用这两个栈类成员变量的构造函数。

  • 每个成员变量在初始化列表中只能出现一次,语法理解上初始化列表可以认为是每个成员变量定义初始化的地方
    • 由于某些原因,C++规定必须给每一个成员变量找一个定义的地方,比如被const修饰的变量、引用变量它们在定义的时候必须初始化。若是在函数体内实现,每个成员可以出现多次不能确定那块位置是否为成员变量的初始化。
class A
{
public:
	A(int n = 4)
		: _n(new int[n])
		, _a(n)
		, _b(n)
	{}
private:
	int* _n;
	int& _a;
	const int _b;
};

若没有使用初始化列表完成 _a、_b成员变量的初始化,编译时会出现这种报错。

error C2530: “A::_a”: 必须初始化引用 error C2789: “A::_b”: 必须初始化常量限定类型的对象

多次初始化编译器也会提示你,某变量已经初始化了。

  • C++支持在成员变量声明的地方给缺省值,给没有在初始化列表显示初始化的成员变量使用

给缺省值的同时也对成员变量在初始化列表初始化

class B
{
public:
	B(int n = 4)
		: _n(new int[n])
		, _a(n)
		, _b(n)
		, _st(4)
	{}
private:
	int* _n = new int;
	int& _a;
	const int _b = 2024;
	Stack _st = 10;
};

在这里插入图片描述

可以发现,运行结果都是按照初始化列表给定的值完成初始化。在这里插入图片描述

给定缺省值,不在初始化列表初始化,编译器会自动调用缺省值使成员变量在初始化列表完成初始化。

  • 尽量在初始化列表实现成员变量的初,始化,如果咱不写编译器也会将其放在初始化列表初始化。内置类型随机赋值或连续赋值,自定义类型调用它的默认构造

  • 初始化列表按照成员变量声明的顺序进行初始化,所以在初始化列表中最好按照声明顺序进行定义别把顺序打乱了。

二、类型转换

2.1隐式类型转换

造构造函数中不仅可以构造初始化对象,对于单个参数或者第一个参数无缺省值其余参数均有默认值的构造函数还具有类型转换的作用

class Date
{
public:
	Date(int year)
		:_year(year)
		{}
private:
	int _year;
	int _month = 3;
	int _day = 7;
};
int main()
{
	Date d1 = 1;
	return 0;
}

上述代码将一个整形赋值给一个自定义类型,这种逻辑很明显是错误的,但是Date类实现了,单个参数的构造函数,使得整形1在赋值给d1对象时,发生了隐式类型转换,将整形1转换为Date类型,从而进行赋值。

而实际的流程:Date里实现的有通过整形为形参实现的单参数构造函数,尝试使用一个int对象给一个Date对象赋值后,编译器使用int对象调用构造函数生成一个Date类型的临时对象,最后通过临时对象调用拷贝构造函数实现对Date对象的赋值。

  • 构造函数–生成临时对象–调用拷贝构造–完成赋值

对于VS编译器,调用构造函数 + 拷贝构造函数的过程,会被优化为:调用构造函数。这是编译器在效率上的优化提升,不同版本编译器的优化效果和逻辑大不相同,类如DEVC++编译器可能就不会去进行优化。


注意:编译器生成的临时对象具有常性

如下图:前面提过,隐式类型转换实际上通过调用构造函数生成临时对象,而临时对象又去调用拷贝构造完成赋值的过程,这里的临时对象具有常性,下图的**普通d1对象尝试对一个具有常性的对象进行引用,将会导致权限放大,**从而引发编译器报错。

在这里插入图片描述

将d1对象使用const修饰后就不会出现这种问题
在这里插入图片描述

但这又引出了新的问题,临时对象在调用完拷贝构造函数,出了作用域它就会被销毁了,而常性d1对象对临时对象使用了引用,这导致d1对象对一块被销毁引用,是野引用.

2.2内置类型的类型转化

对于内置类型的转换,实际上也存在生成临时对象,然后完成拷贝赋值的过程。

int main()
{
    //类似的还有
    int i = 1;
    double j = i;//ok~没问题隐式类型转换
   
    double& j = i//ok~这就有问题,i生成的临时对象具有常性,j想指向它导致权限放大,编译器报错。
    const double& j = i;//使用const修饰,权限平移。
    return 0;
}

2.3explicit关键字

当不期望,隐式类型转换的发生,可以在构造函数前添加关键字: ecplicit

它的功能:禁止使用隐式类型装换

在这里插入图片描述

2.4多参数构造

以上介绍的都是单参数的类型转换,而多参数的构造实现后,该如何赋值?

class Date
{
public:
	Date(int year, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
private:
	int _year;
	int _month = 3;
	int _day = 7;
};
int main()
{
	Date d1 = 2024,10,1;//奇奇怪怪的赋值方法,从来没有见过,当然编译器也不支持这种写法
	return 0;
}

多参数的类型装换需要使用 {}花括号 Date d1 = {2024, 10, 1};,C++11才开始支持

int main()
{
    Date d1 = {2024, 10, 1};
	return 0;
}

同理若是,多参数的构造函数不期望支持隐式类型转化,使用关键字 explicit修饰即可。

三、static成员

  • 用static修饰的成员变量,称为静态成语变量,静态成员变量一定要在类外面进行初始化
  • 静态成员变量为所有类成员共享,不属于某个具体的对象,不存在类中,存在静态区
class pp
{
private:
	int _z;
	int _y;
	static int _x;
};
int pp::_x = 1;
  • 用static修饰的成员函数,称之为静态成员函数,静态成员函数没有this指针
  • 非静态的成员函数,可以访问任意位置的成员变量和成员函数
    • 静态成员函数,只能访问静态成员变量和静态成员函数,它没有this指针
class pp
{
public:
	pp(int z = 3, int y = 2)
		:_z(z)
		,_y(y)
	{}
	static int Get_x()
	{
		_z++;
		return _x;
	}
    void Fun()
	{
		_x++;
		_y++;
	}
private:
	int _z;
	int _y;
	static int _x;
};
int pp::_x = 1;

类如上述静态成员函数,它被static修饰后就没有this指针,在 Get_x 函数内调用就回引起编译器报错。 error C2597: 对非静态成员“pp::_z”的非法引用

Fun函数就不会有什么问题,它并不是静态成员函数,函数内部使用this指针,和静态成员变量都是正确的。

  • 静态成员也受 private、public、protected访问限定符的限制

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

    class pp
    {
    public:
    	pp(int n = 1)
    	{
    		_z = n;
    		_y = n;
    	}
    private:
    	int _z;
    	int _y;
    public:
    	static int _x;
    };
    int pp::_x = 10;
    int main()
    {
    	pp p;
    	cout << p._x << endl;
    	cout << pp::_x << endl;
    	return 0;
    }
    

若静态成员变量收限定符的限制,无法通过类名或对象类进行访问,此时可以写一个get函数来获取静态成员函数的大小。

  • 静态成员变量不能在声明位置给缺省值初始化,这个缺省值是构造函数初始化列表使用的,而静态成员变量不属于某个对象,不能走构造函数初始化列表。
class A
{
private:
    static int _count;
}
int A::_count = 1;

int main()
{
    A a1;
    A a2;
    A a3;//a1、a2、a3都可以访问
    cout << A::_count <<endl;
    cout << a1._count << endl;//没有被private修饰,两种突破类域的方法
    
    a1.GetCount();//或者在类中实现获取count的函数。
    return 0;
}

四、友元

友元,是一种用于突破类访问限定符封装的方式,通过它就可以使外部函数访问类的私有和包含成员。而友元分为:友元函数、友元类。在函数声明或类声明的位置前面加friend,并将其放在一个类中。

  • 友元函数/类可以访问类的私有和保护成员,友元函数仅仅是一种声明,他不是类的成员。
class Date
{
    
    
	friend ostream& operator<<(ostream& out, const Date d);
public:
	Date(int year, int month, int day)
	{
    
    
		_year = year;
		_month = month;
		_day = day;
    }
private:
	int _year;
	int _month;
	int _day;
};
ostream& operator<<(ostream& out, const Date d)
{
    
    
	cout << d._year << "年" << d._month << "月" << d._day << "日" << endl;
}
  • 友元函数/类可以在类定义的任何地方使用,不受访问限定符限制
  • 一个函数可以是多个类的友元
#include <iostream>
using namespace std;


class hh;//前置声明
         //若不做声明,pp中的友元函数声明不认识hh类
class pp
{
	friend void Fun(const pp& p, const hh& h);
public:
	pp(int n = 2)
		: _a(n)
		, _b(n)
	{}

private:
	int _a;
	int _b;
};
class hh
{
	friend void Fun(const pp& p, const hh& h);
public:
	hh(int n = 5)
		: _m(n)
		, _n(n)
	{}

private:
	int _m;
	int _n;
};
void Fun(const pp& p, const hh& h)
{
	cout << h._m << endl;
	cout << p._a << endl;
}

  • 友元类的关系是单向的,A类是B类的友元,B类不是A类的友元。

    • 就好比张三和李四是好盆友,李四和王五是好朋友,但这并不代表王五是张三的好朋友,他们两个认不认识还不一定
  • 友元类的关系不能传递,A类是B类的友元,B类是C类的友元,但A类不是C类的友元。

    • 类似友人B找了两个女朋友,分别是友人A、友人C,这时候友人B脚踏两只船。

      而友人B可不希望两个女朋认识,友人B都给女朋友说我只爱你一个人,然而呢~,某一天友人B和友人A出去逛商场的时候恰巧碰见了友人C,诶哟我去!这个场面给位自己想象吧 ~,嘿嘿

  • 友元的存在增加了耦合度,破坏了封装,不宜多用。

五、内部类

如果一个类定义在另一个内的内部,那就将这个类称之为内部类

概念:

内部类是一个独立的类。内部类与定义在全局相比,内部类受到了外部类类域限制和访问限定符的限制,外部类定义的对象中不包含内部类

下列,在类M中定义了一个N类,那这个N类就是内部类

class M
{
public:
	M(int n = 3)
		:_a(n)
		,_b(n)
	{}
private:
	int _a;
	int _b;

public:
	class N
	{
	public:
		void Fun(const M& m)
		{
			cout << m._a << endl;
		}
    private:
        int _x;
	};
};
int main()
{
    M m;
	cout << sizeof(m) << endl;
    return 0;
}
  • 内部类天生就是外部类的友元,内部类可以通过外部类的对象参数来访问外部类中所有成员,但外部类不是内部类的友元

  • N类包在M类里面,若需要单独实例化N类,将会收到外部类类域的限制,此时需要使用域作用限定符 ::
    在这里插入图片描述

内部类的特性

  • 内部类定义在外部类的public、private、protected都是可以的
class pp
{
public:
	class A
	{
	private:
		int _a;
	};
private:
	class B
	{
	private:
		int _b;
	};
protected:
	class C
	{
	private:
		int _c;
	};
};
  • 由于内部类是外部类的友元,内部类在访问外部类的不需要加 类名/对象名

  • 内部类并不会占用外部类的空间容量,使用sizeof计算外部类的大小不会与内部类有关

  • 若两个类的关系紧密相连,B类的功能主要提供给A类使用,就可以考虑将B类实现为A类的内部类。若给内部类使用访问限定符修饰(private/protected),那这个内部类就是外部类的专属,其余类无法使用。

六、匿名对象

匿名对象,没有类名的对象。语法:类名()

可以发现,在下列代码中定义了两个对象:一个有名字,一个没有名字。没有名字的对象就称为匿名对象。

#include <iostream>
using namespace std;
class C
{
public:
	C(int n = 10)
	{
		cout << "调用构造" << endl;
		_c = n;
	}
	~C()
	{
		cout << "调用析构" << endl;
	}
	int ret() { return _c; }
private:
	int _c;
};
int main()
{
	C c1(66);
	C(6);
	return 0;
}

首先介绍匿名对象特别重要的一个特性:匿名对象的声明周期只有当前行,当编译器读到下一行代码时,匿名对象就会被自动销毁了。

在这里插入图片描述

  • 没有对象名的匿名对象也可以调用成员函数

在这里插入图片描述

  • 匿名对象的引用

    • 匿名对象具有常性,被引用需要将const修饰,这样做它的生命周期被延长了,不会即用即销毁,在程序运行结束后才会销毁。const A& r = A(); 但它被引用后而匿名对象的空间被销毁,导致野引用,这在语法上是不允许的
    • 反之,没有使用const进行修饰,在编译时会引起编译器的报错

在这里插入图片描述

匿名对象作函数缺省值:

class A
{
public:
	A(int n = 2)
        :_a(n)
    {}
    ~A()
    {
        cout << "~A()" << endl;
    }
private:
    int _a;
};
void fun(A aa = A(1))//函数缺省值,给匿名对象
{
    
}
int main()
{
    A();//这是一个匿名对象。没有参数也必须加上括号
    A(10);//这是一个匿名对象。
    return 0;
}

在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/sparrowfart/article/details/142989371