之前的章节中,学习到了类的基本定义方式,访问限定符,类实例化的方式;以及六个默认成员函数:构造函数、拷贝构造函数、析构函数、赋值运算符重载、取地址运算符重载。下面继续探讨类与对象的其它一些特性。

一.初始化列表

        针对构造函数,继续进行深入探讨,当在类中定义了以下三种变量时,构造函数函数体内赋值的方式将会发生编译错误:const成员变量、引用类型成员变量、没有默认构造函数的类成员。

        而发生此编译错误的原因就是,构造函数的函数体内并不是类成员初始化的地方,而是重新赋值的地方。类成员真正初始化的地方是——初始化列表

1.1基本语法

        初始化列表的基本语法:在构造函数名之后,函数体的{}之前,以冒号开始,逗号分隔,初始化对象不用=,而是初始化对象(初始化值),最后不需要分号,如下面这段代码所示:

class Date
{
public:
	Date(int& x ,int year = 2000, int month = 1, int day = 1)
		//初始化列标这里,相当于在实例化对象的时候进行了对变量的初始化
		: _year(year)
        , _month(2)
	{
		//构造函数的内部对象,本质相当于进行重新赋值
		_day = day;
	}

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

        这个类中,实现了一个最基本的初始化列表,其对三个变量都进行了初始化。_year赋值为构造函数的形参year;_month赋值为2;_day虽然没有在初始化列表中写出,但其也通过了初始化列表的初始化,会被编译器默认赋值为0或者随机值。函数体内对_day赋值为形参day,本质相当于对实例化后的_day成员变量进行重新赋值。

        根据上面的理论,就可以得知:构造函数的函数体内,并不是实例化对象初始化数据的地方,而是重新赋值的地方,所以就得知针对在初始化时必须指定值的类型——引用、const变量,这两种类型必须在初始化列表中进行初始化。

        而针对没有默认构造函数的类成员——没有默认构造函数指的不是没有构造函数,而是没有:全缺省、无参数和自动生成的构造函数。指的是必须通过传参调用构造函数的类型,也必须要通过初始化列表来初始化,如下面的例子:

class A
{
public:
	//这个构造函数,不是默认构造函数,因为不是:参数全缺省、无参数、编译器默认生成的
	A(int x ,int y )
	{
		_a = x;
		_b = y;
	}
private:
	int _a;
	int _b;
};

class Date
{
public:
	Date(int& x ,int year = 2000, int month = 1, int day = 1)
		//初始化列标这里,相当于在实例化对象的时候进行了对变量的初始化
		: _year(year)
		, _a(x)//引用类型的成员变量,必须在初始化列标初始化
		, _b(10)//const修饰的成员,必须初始化列表中初始化
		,a(8,9)//类对象没有默认构造函数,需要初始化列标中初始化
		,ptr((int*)malloc(13))//初始化列表的写法
	{
		//构造函数的内部对象,本质相当于进行重新赋值
		_day = day;
	}
	friend ostream& operator<<(ostream& out, const Date& d);

private:
	int _year;
	//这里给2,是初始化列表的缺省值。一定注意这里是初始化列表的缺省值。如果初始化列表不写,才会走缺省值。
	int _month = 2;
	int _day;

	int& _a;
	const int _b;
	A a;
	//初始化列表中初始化的顺序与类中成员变量的声明顺序相同。
	//无论写不写初始化列表,是一定会走初始化列表的。
	int* ptr = (int*)malloc(12);//缺省参数
}; 

        如这段代码,创建了一个类A,内部有两个成员变量_a 、_b,其构造函数不是默认构造函数。在Date的类中,成员变量有一个类型A的类变量,但A没有默认构造函数,所以对类型A的变量a,必须要在初始化列表中初始化,不能在构造函数函数体内进行赋值。

       同时也可以在上面的代码中注意到,在成员变量声明的位置给了一个初始值,该值叫做缺省值,是给初始化列表使用。当初始化列表中没有没有针对该成员的初始化时,会自动调用缺省值来进行初始化。但是当初始化列表中有关于该变量的初始化时,就不会在利用关于缺省值的任何数据。

        同时要注意,通过对程序的调试,可以发现,初始化列表中队成员变量的初始化顺序,是按照类成员的声明顺序,不会根据初始化列表中的初始化顺序而改变。

        

下例一个练习题:

class A
{
public :
	A(int a)
		//初始化列表这里,先对_a2初始化
		//此时_a1为随机值,之后在对_a1
		//a传参为1,所以_a1是1
		//输出结果为_a1 = 1;_a2 是随机值或0
		:_a1(a)
		,_a2(_a1)
	{
	}
	void print()
	{
		cout << _a1 << " " << _a2 << endl;
	}
private:
	int _a2 = 2;
	int _a1 = 2;
};
int main()
{

	A aa(1);
	aa.print();
	//输出结果为
	return 0;
}

        如这段代码例题,有一个类型A,内部声明了两个成员变量,_a2先声明有缺省值2,_a1后声明有缺省值2。A的构造函数为非默认构造函数,初始化列表中,用_a1的值初始化_a2,用构造函数形参a初始化_a1。

        声明顺序为_a2,_a1,所以初始化顺序为_a2   _a1,而由于在初始化列表中进行初始化,所以二者均不利用缺省值。此时先对_a2初始化,_a1为随机值,所以_a2被初始化为随机值,_a1被初始化为构造函数的形参值1。

        输出时,先输出_a1,再输出_a2,所以输出为1 和随机值。

        最后用一个初始化列表的总体结构图来总结:

        实例化类对象成员的初始化:首先经过初始化列表,根据初始化列表中的值来进行初始化;如果没有再初始化列表中显示写明赋值的值,如果该数据有缺省值,那么按照缺省值来进行初始化;如果没有缺省值:内置类型的变量(int、double等)会被赋值为随机值或0;其他类型则会调用其他类型相应的默认构造函数,如果没有默认构造函数,就会编译报错。

二.类型转换        

        类型转换在C++初始,引用的章节已经提到过,当时的举例为:

int main()
{
    double a = 2.2;
    const int& ra = a;
    
    return 0 ;
}

        这里本质是将double类型的a,转换为int类型,存储在一个临时变量中,而由于临时变量具有常型,所以其引用需要是常引用。

         而在C++中,支持内置类型向类类型的转换,但是需要类内提供该内置类型为参数的构造函数:如下例:

        

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

private:
	int _a = 1;
};

class B
{
public:
	B(int x , int y)
		: _a(x)
		, _b(y)
	{
		cout << "B(int)" << endl;
	}

	int GetNum()
	{
		return _a + _b;
	}

private:
	int _a = 1;
	int _b = 2;
};

class C
{
public:
	//构造函数前加explicit就不再可行
	explicit C(int x)
		: _c(x)
	{
	}

private:
	int _c = 1;
};

class D
{
public:
	D(B b)
		:_d(b.GetNum())
	{
	}

private:
	int _d = 0;
};

int main()
{
	//这里的a = 1,就是利用构造函数来实现类型转换
	//注意这里并不是单纯的利用构造函数
	//而是首先将1当作类对象进行一次构造函数
	//再进行一次拷贝构造赋值。
	A a = 1;
	//A a(1)  注意区分

	//多参数的类型转化。
	B b = { 1,2 };
	
	//C c = 1;//不可行
	D d = b;

	//这么做的目的,主要是为了,以后需要只进行一个数据传递给类的时候,不再需要创建一个新的类,而是直接传数据,进行类型转换即可。
	return 0;
}

        首先只看class  A,内部提供了一个构造函数,形参为int类型,这样就支持int类型的数据向A类型的转换,具体语法如main函数中展示:A a = 1,这里本质是利用1,调用构造函数构造了一个A类型的对象,存储在临时变量中,然后再调用拷贝构造函数赋值给对象a。

        本质是调用了构造函数和拷贝构造函数两个构造函数,VS中将其优化为仅调用构造函数。

        下面第二个代码,如果向命名一个A类型的引用,那么需要为常引用,因为存储在临时变量中的变量具有常性。

        再看类型B,构造函数中定义了两个整形,那么B类型的创建就支持两个整形数据的类型转换,其格式如main函数中对B类型对象b的赋值:B b = { 1,2 },注意是大括号,而不是小括号。

        如果在构造函数前加explicit,那么就不再支持类型转换,如类型c所示,C c = 1会产生编译错误。

        同时C++不仅支持内置类型向类的转换,也支持类-类之间的类型转换,只需要提供以相应类型的构造函数即可。如类型D。

三.static静态成员变量和静态成员函数

        用static对类内的成员变量进行修饰,那么该成员变量就变成了静态成员变量。静态成员变量真正存储在静态区,与类中的其他成员变量不同,不随着实例化的创建而创建,其一直存在,直到程序结束。

        从这里也可以推断出,静态成员变量不随着类实例化时进行初始化,所以其不通过初始化列表途径进行初始化。需要在类外指定其初始化

        

class A
{
	//实现了一个类,可看出其调用了多少次构造、拷贝构造、赋值运算符重载
public:
	A(int x = 1)
	{
		_count++;
	}

	A(const A& a)
	{
		_count++;
	}


	static int& GetCount()
	{
		return _count;
	}

private:
	static int _count;
};

int A::_count = 0;

int main()
{
	A a;
	cout << a.GetCount() << endl;
	A b(2);
	cout << a.GetCount() << endl;
	A c = a;
	cout << A::GetCount() << endl;

	return 0;
}

        如这段代码,定义了一个类A,其内部有一个静态成员变量_count,需要在类外进行定义初始化,方式为:int A::_count = 0;需要为其指出类域和数据类型,从而进行初始化。

        这个类实现了统计该类共出现了多少次构造和拷贝构造的次数。

        同时该段代码中出现了一个用static修饰的GetCount函数,其实现了返回静态成员变量_count的值。利用static修饰的成员函数叫做静态成员函数,其最大的特点时没有this指针,所以只能访问静态成员变量而无法访问非静态的其它成员变量。因为成员函数访问成员变量的本质是:this->成员变量,所以静态成员函数无法访问静态成员变量。

        但是非静态的成员变量具有this指针,所以既可以访问非静态成员变量,又可以访问静态成员变量。

        非静态成员函数和成员变量访问时,需要有一个实例化对象利用 . 来访问。而静态成员函数和静态成员变量由于不随着实例化的创建而创建,所以可以直接利用指定类域来访问。

        根据静态成员变量的特点,下面讲解一道例题:        

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

        解决这道例题,需要利用到static静态成员变量:

        代码如下:

#include <climits>

class Solution {
private:
    class Sum
    {
    public:
        Sum()
        {
            _ret+=_i;
            _i++;
        }
    };
public:
    int Sum_Solution(int n) {
        Sum s[n];

        return _ret;
    }

    static int _i;
    static int _ret;

};

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

        首先定义Solution类,用来解决该问题,创建该类的对象,直接调用类内的Sum_Solution函数并传参n即可。

        在Solution类内部,定义了两个静态变量,_i 用来代表每次累加的值,_ret用来表示累加的结果。

        在Solution类的内部,定义了一个Sum类,用来计算累加结果。在Sum_Solution函数内部,创建了一个具有n个Sum类型元素的数组,所以相当于创建了n个Sum类型数据的空间,就会调用n次Sum类中的构造函数,而调用Sum类中的构造函数就会实现累加的操作,并且由于静态成员变量的特性,_i 每次增加1,_ret实现累加的操作,最后只需要返回_ret即可。

四.友元

        友元在类与对象(中)讲解流插入提取的操作符中已经第一次提到:如果将流插入、提取操作符定义在类内,由于this指针为运算符重载函数的第一个参数,就会变成 例如 :d<<cout。所以要将流插入提取的运算符重载定义在函数外面,而由于类中的成员变量一般为私有,所以解决方法就是在类内部将该函数声明为友元函数,这样流插入、提取运算符重载就可以调用类内的成员函数。

        友元是一种突破类中访问限制符限制的方法,分为友元函数和友元类。只需要在函数或者类前面加上friend,声明在类中的任意位置即可。

        一般来说,友元函数定义在外部,其并不是类内的成员函数,只是可以利用类中的成员变量。其可以声明在类中的任意位置,不受访问限制符限制。

        


class B;
class A
{
public:
	A()
		:_a(1)
	{}

	friend int Func(const A& a, const B& b);
private:
	int _a;
};

class B
{
public:
	B()
		:_b(2)
	{}

	friend int Func(const A& a, const B& b);
private:
	int _b;
};

int Func(const A& a, const B& b)
{
	return a._a + b._b;
}

int main()
{
	A a;
	B b;
	cout << Func(a, b) << endl;

	return 0;
}

        如这段代码所示,定义了两个类A B,同时定义了一个A  B共同的友元函数Func,参数分别为类型a和类型b,所以就需要在A和B内共同声明Func这个友元函数。而由于A先定义,所以在A中定义的Func无法找到B,所以需要对类B也进行一个声明。这是友元函数的用法。

        同时可以声明友元类,友元类的成员函数默认均为该类的友元函数,所以友元类中的成员函数全都可以访问到私有成员变量。

        

class A
{
	friend class B;
private:
	int _a1 = 1;
	int _a2 = 2;
};

class B
{
public:
	void fun1(const A& a)
	{
		cout << _b1 << " "<< _b2 << endl;
		cout << a._a1 <<" "<< a._a2 << endl;
	}

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

int main()
{
	A a;
	B b;

	b.fun1(a);
}

        如这段代码,定义了类A和类B,在A中声明B是A的友元类,所以B的函数均默认为A的友元函数,即在B内定义的函数fun1可以访问到A中的_ a1  和  _a2,在main函数中实例化对象a 和b,利用b调用函数fun1,并传参给对象a,可以在打印界面看到输出了_a1  和  _a2的值。

        在这里需要注意,B是A的友元,该友元过程是单项的,B可以访问到A中私有限制的成员变量,但是A却不可以访问到B中私有的成员变量,具有单向性。

        同时,如果A是B的友元,B是C的友元,那么A并不是C的友元,友元不具有传递性。

        虽然友元能突破访问限制符限制,但是其破坏了耦合性,不宜多用。

五.内部类

        如果一个类定义在另一个类的内部,那么就叫其为内部类。

        要注意的是,内部类并不属于外面的类,而是一个独立的类型,只是受限于外面类的访问限制符和类域。如下面代码展示:

class A
{
public:
	class B
	{
	private:
		int _b1 = 1;
		int _b2 = 2;
	};
private:
	int _a1 = 1;
	int _a2 = 2;
};

int main()
{
	A a;
	A::B b;

	cout << sizeof(A) << endl;
	cout << sizeof(A::B) << endl;

	return 0;
}

        B是A的内部类,所以在定义B时,要先指定A类域内的类B,才可以进行实例化对象。通过sizeof输出A的大小和B的大小可以发现,A的大小为8,为两个整形即_a1和_a2的大小,从这里也可以看出类型B不属于类型A,其只是受限于A的类域。

        如果想让B只能在A中创建和调用,只需要将B限制为private私有化即可。

        内部类默认为外部类的友元类,对于上面的示例来说,B是A的友元类,所以B可以调用A的私有化成员变量,但是A不可以调用B的私有化成员变量。

        上面的的例题,就是用内部类来实现1+2+。。。+n。

六.匿名类

        直接用类加括号创建出的对象叫做匿名类,匿名类的生命周期仅在当前这一行。

如下面代码所示:

class A
{
public:
	A(int a = 1)
		: _a(a)
	{
		cout << "A(int a =1)" <<_a<< endl;
	}

	~A()
	{
		cout << "~A()" <<_a<< endl;
	}

private:
	int _a;
};

int main()
{
    A a;
	A a(1);
	//A a()  //err
	
	A(2);
	A();
	return 0;
}

        创建了一个类A,内部存在一个成员变量_a,其构造函数给_a赋值,并输出一个标识字符出啊,析构函数输出要给标识字符串。

        在主程序中,如果创建一个对象,需要如第一行所写的那样,类型+对象名+赋值,注意不能写成A a(),这样会导致编译器报错,因为这样写语法会和函数声明无法区分。

        下面的A(2) 和A(),都是匿名类,调用程序会发现其在当前行结束就运行了析构函数,生命周期只在当前行。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐