类与对象(下)
之前的章节中,学习到了类的基本定义方式,访问限定符,类实例化的方式;以及六个默认成员函数:构造函数、拷贝构造函数、析构函数、赋值运算符重载、取地址运算符重载。下面继续探讨类与对象的其它一些特性。
一.初始化列表
针对构造函数,继续进行深入探讨,当在类中定义了以下三种变量时,构造函数函数体内赋值的方式将会发生编译错误: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(),都是匿名类,调用程序会发现其在当前行结束就运行了析构函数,生命周期只在当前行。
更多推荐


所有评论(0)