1、const成员
看一段代码
class A
{
public:
void Print()
{
cout << _a << endl;
}
private:
int _a = 10;
};
int main()
{
A aa;
aa.Print();
return 0;
}
如果const A aa,那么就报错了。aa传过去自己的地址是类型是const A* &aa,Print函数能接受的参数类型应当是A* this,所以权限放大了,我们只要在类里面加上const。this指针的类型无法改变,因为他是一个隐含参数,所以要在函数上加上const,同样这也就限制了传进来的参数不能改变。
void Print() const
{
cout << _a << endl;
}
对于内部不改变成员变量的成员函数,最好const修饰。在上篇的日期类中,比较函数,Print等也可以加上const。
2、取地址及const取地址操作符重载
这个重载也是默认成员函数,它也会自动生成,如果需要访问到指定的内容,就需要手动写。
const A aa;
aa.Print();
cout << &aa << endl;
手动写默认函数可以这样
A* operator&()
{
return this;
}
const A* operator&() const
{
return this;
}
但没必要,自动生成的就够了。但有一些特殊需求就得自己写。有的时候,普通版本和const版本都需要写。
3、构造函数续集
1、初始化列表
如果这样写类,
class A
{
public:
int _a1;
int _a2;
const int _a3;
};
int main()
{
A aa;
return 0;
}
程序就会报错,显示有未初始化的成员,就是_a3。为什么默认构造函数没有初始化它?系统虽然处理内置类型,但不处理const修饰的类型,所以const修饰的变量需要我们在定义处初始化,A aa是对类这个整体的定义,里面的成员会依据构造函数处理,而const修饰的成员变量要在外围单独初始化。当然类里面给const修饰的变量缺省值也可。
不过这样写不够好,C++的构造函数要把所有变量都初始化,也就出现了初始化列表,对特殊变量的初始化放在一起。即使main函数体里也有对特殊变量的初始化,也还是按照类里的去定义,因为初始化只有一次,再之后不会再初始化。
A()
:_a3(2)
{
_a1++;
}
把对_a1的使用放在声明之前,也没问题,都在类里,编译器还是会按照_a1的初始化值去++。初始化列表用冒号开头,中间连接用逗号
class A
{
public:
A()
:_a3(2)
, _a2(1)
{
_a1++;
}
void Print()
{
cout << _a1 << endl;
cout << _a3 << endl;
cout << _a2 << endl;
}
private:
int _a1 = 1;
int _a2 = 2;
const int _a3;
};
打印的结果是2 2 1。这里的规则是
初始化列表是它所有成员变量定义的位置
不管是否显示在初始化列表,编译器的每个变量都会被初始化列表去初始化
初始化列表有对某个变量的初始化,那就按照它给的值去执行,如果没有,那就按照在声明时是否给了缺省值或者构造函数的初始化,都没有,那就只能是随机值。
C++规定const修饰的变量,引用类型的变量,用到其他类里的成员变量必须在初始化列表里初始化。用到其他类的成员变量,也可以说是没有构造函数的自定义类型成员,它会是这样存在的
class B
{
public:
B()
:_b(0)
{
cout << "B" << endl;
}
private:
int _b;
};
class A
{
public:
A()
: _x(1)
, _a2(1)
, _ref(_a1)
{
_a1++;
_a2--;
}
private:
int _a1 = 1;
int _a2 = 2;
const int _x;
int& _ref;
B _bb;
};
那再调用aa,Print()时就会打印出B,因为对于自定义类型,编译器会调用它的默认构造,在A的初始化列表中,就会来到B这里,调用它的初始化列表。所以会打印B。
如果要往B传入参数
B(int _b)
:_b(0)
{
cout << "B" << endl;
}
就不会打印,会报错B没有合适的默认构造函数,这里就得用全缺省,在括号中写上int _b = 1,打印_b会得到1。另一个办法就是不提供全缺省,而是在A的初始化列表中初始化。
A()
:_a3(2)
, _a2(1)
, _bb(1)
{
_a1++;
_a2--;
}
在A的初始化列表中给了bb什么值,那么B中打印_b就会打印什么值。在初始化列表的变量列表都可以去找初始化的办法,所以写代码的时候可以把所有初始化都放在初始化列表里。
在类里,初始化的顺序是按照声明的顺序去做的,所以这方面也要注意。
2、explicit关键字
现在看这一段代码
class A
{
public:
A(int a)
:_a1(a)
{
cout << "A(int)" << endl;
}
A(const A& aa)//这里是拷贝构造,拷贝构造也有初始化列表
:_a1(aa._a1)
{
cout << "A(const)" << endl;
}
void Print() {
cout << _a1 << ednl;
}
private:
int _a1;
};
int main()
{
A aa1(1);
A aa2 = 1;
return 0;
}
对于aa2来说,这是一个隐式类型转换,1去创建一个A类型的临时变量,然后这个变量再去拷贝构造aa2这个变量。类里已经有了默认拷贝构造函数,但是打印结果的时候却没有A(const),没有调用拷贝构造,是因为编译器把它变成了用1来做构造,相当于aa2(1),减少工作量。如果A& ref = 10,无法执行,因为无法从int转换到A&。
这样写: const A& ret = 10。只有加上const,才没有错误。这时候就是一个类型转换,引用针对的就是生成的临时变量,就会调用那个拷贝构造。但是在VS2019中,还是会打印A(int),新一些的编译器估计也是会优化。
如果不想类型转换,那么就在函数前加上explicit,那么aa2 = 1和 const A& ret = 10就不可以转换了。这个对于单参数构造函数是有用的。多参数构造函数就需要这样用。
A(int a1, int a2)
:_a1(a1)
,_a2(a2)
{}
private:
int _a1;
int _a2;
A aa2(1, 1);
A aa3 = { 1, 1 };
const A& ret = { 1, 1 };
这样也会调用拷贝构造。
4、static成员
创建一个类,计算创建了多少个类对象。
我们可以用构造函数来计算。假设用一个全局变量来存储数值,这是不行的,会和库里的变量冲突,我们可以不展开命名空间using namespace std;用什么展开什么
using std::cout using std::endl。但是全局变量是难以控制的。C++为了解决这个问题,把这个变量放到了类里面,并且加上了static。
class A
{
public:
A(int a = 0)
{
++count;
}
A(const A& aa)
{
++count;
}
static int count;//属于所有对象,属于整个类
};
但是不能在类里面初始化这个值,这个变量是共有的,应当在类外面初始化
int A::count = 0。在外部想要打印时可以这样
cout << A::count << endl;
cout << aa2.count << endl;
A* ptr = nullptr;
cout << ptr->count << endl;
不过如果count为私有变量,上面几个方式就不行。那么我就可以在类里面写个函数,外边调用这个函数即可。
假设没有创建对象,比如把创建的代码放在一个函数里,而不是在main函数里。
void func()
{
A aa1;
A aa2(aa1);
A aa3 = 1;
}
还是用static,让类里计算数值的函数变成静态函数,这样的优势就是没有this指针,它是静态区的,在外面就可以用A::函数名来调用。
int main()
{
func();
cout << A::GetCount() << endl;
}
静态函数里不能使用类里的非静态变量,因为没有this,无法调用这些变量。
静态函数服务于静态变量,其它的不得插手。
void func()
{
A aa1;
A aa2(aa1);
A aa3 = 1;
A aa4[10];
}
这个的调用次数就是13,因为aa4给数组里的10个元素都初始化了,也就是调用了10次。
5、匿名对象
创建类对象时,不能A aa(),因为编译器不知道这是函数还是类,但可以这样写,A(),这是匿名对象。匿名对象的生命周期只在这一行,它没有名字,可以调用成员函数,可以把它当做正常的类对象使用,但仅仅在这一行,到了下一行它就没有了。
6、友元
1.友元函数
友元之前写过,分为友元类和友元函数,虽然可以突破封装,可以作为一个类外部的函数访问类的私有成员,在类里用friend关键字声明即可。但它破坏了封装,所以尽量少用
友元函数特征:
友元函数可访问类的私有和保护成员,但不是类的成员函数
友元函数不能用const修饰
友元函数可以在类定义的任何地方声明,不受类访问限定符限制
一个函数可以是多个类的友元函数
友元函数的调用与普通函数的调用原理相同
2、友元类
甲类是乙类的友元类,那么甲类就可以直接访问乙类的私有成员。但是乙类不能访问甲类。
7、内部类
内部类就是在类的里面再定义一个类。
class A
{
private:
int a;
public:
class B
{
private:
int b;
};
};
int main()
{
A aa;
cout << sizeof(aa) << endl;
return 0;
}
结果是4。调试起来后查看aa的内容,也只有一个a。这个内部类B和定义在全局的B没有什么不同,空间上是独立的,但它受A的类域限制,在外部只能指定访问,A::B,但如果内部类是私有的,也不能访问。
内部类B是A的友元,也就是B里面可以直接访问A的私有成员。
结束。