文章目录
- 前言
- const成员函数
- 取地址及const取地址操作符重载
- 构造函数续
- explicit
- static成员
- 友元
- 内部类
- 匿名对象
前言
在前面的文章中,我们了解到了类的四个默认成员函数:构造、析构、拷贝构造和赋值重载。接下来我们会继续学习剩下的两个默认成员函数以及类和对象的一些细节语法。
那么在了解剩下的两个成员函数前,我们先来学习一下const成员函数。
const成员函数
首先有如下场景:
class A
{
public:
void Print()
{
cout << "Success" << endl;
}
private:
};
int main()
{
const A a;
a.Print();
return 0;
}
如上我们只是想调用A的print函数,这个函数不会改变任何值,但实际上我们运行不了这个代码。
这又是我们的隐藏this指针的锅了,每个成员函数的参数里面都有一个隐藏的this指针参数,更关键的是这个this指针没有被const修饰哦!
这又是一个典型的权限放大了,我们的a是被const修饰的,但是this指针却没有被const修饰,因此解决方法就是让this指针也被const修饰,具体方法也很简单:“)”后加const即可。
class A
{
public:
void Print()const
{
cout << "Success" << endl;
}
private:
};
int main()
{
const A a1;
a1.Print();
A a2;
a2.Print();
return 0;
}
由此可知,当成员函数被const修饰时,既可以传入const修饰的类也可以传入没有被const修饰的类。
因此,在成员函数不需要修改类的属性时,最好用const修饰成员函数。
取地址及const取地址操作符重载
那么了解了const成员函数后,我们就可以揭开最后两天默认成员函数的庐山真面目了。
那就是取地址操作符重载和const取地址操作符重载。
没错,由于const成员函数的存在,这个简单的操作符重载竟然分成了两份。其实仔细想想上一个默认成员函数是赋值重载,这个刚好要改变类的属性,因此不能被const修饰。不然六个默认成员函数可能就变成了七个默认成员函数了。
那么这两个默认成员函数存在的意义又是什么呢?
这当然是为了构成逻辑自洽了:由于我们对自定义类型用的操作符往往都是要重载的,而取地址这个操作符不可以说不重要,简直就是c++的灵魂所在,因此编译器会默认生成取地址的重载。因而,我们绝大多场景用编译器默认生成的取地址重载就已经足够了,除非,你想实现一些特殊的功能。比如悄悄传个空地址回去:
class A
{
public:
A* operator&()
{
return (A*)nullptr;
}
private:
};
咳咳,除了这些特殊的场景外。我们是不需要手码这两个默认成员函数的。
构造函数续
在前面我们已经学到了构造函数的一些基础用法,但由于知识的局限性,构造函数的一些其他特点还没有详谈。仔细想想构造函数的定义是不是要我们初始化一个类来着,最终我们的类的属性也的确获得了一些初始值,但那真的是初始化么?
就像这样:
class B
{
public:
B(int x)
{
_x = x;
_x++;
_x = 1;
}
private:
int _x;
};
这里我们对_x进行了多次赋值,最终我们的确得到了_x的值为1,但这完全谈不上是初始化吧。
如果这个案例还不够深刻请看下面的例子:
class B
{
public:
B(int x,int& ry)
{
_x = x;
_x++;
_x = 1;
_ry = ry;
}
private:
int _x;
int& _ry;
};
如上,我们尝试对_ry这个成员变量进行初始化,但貌似这个写法就是前面提及的引用初始化的经典错误写法啊。
关于引用这种变量是要被初始化的,既然上述方式不可行,那真正可行的方式是怎样的呢?
不多卖关子了,如下所示:
class B
{
public:
B(int x, int& ry)
:_x(x)
, _ry(ry)
{
}
private:
int _x;
int& _ry;
};
当我们需要初始化时要写这样一个初始化列表,以“:”开始,以“,”分隔,成员变量(初始化值)。下面的函数体想写什么都可以。
这种略显丑陋的函数,就是我们构造函数真正的初始化方式啦!
它有以下特点:
- 每个成员变量在初始化列表中只能出现一次(初始化只能初始化一次)
- 类中包含以下成员,必须放在初始化列表位置进行初始化:
引用成员变量
const成员变量
自定义类型成员(且该类没有默认构造函数时) - 成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关
也就说:
class C
{
public:
C(int x)
:_x(x)
,_y(_x)
{
}
void Print()
{
cout << _x << ' ' << _y << endl;
}
private:
int _y;
int _x;
};
int main()
{
C c(10);
c.Print();
return 0;
}
正确的做法应该是将x赋给_y,_y赋给_x.
C(int x)
:_x(_y)
,_y(x)
{
}
explicit
谈及构造函数就不得不提一个有意思的东西:隐式类型转换
对于构造函数为单参数的类支持隐式类型转换,
class D
{
public:
D(int x)
:_x(x)
{
}
private:
int _x;
};
D d=1;
上述代码实则是将1转换成了D然后再拷贝构造给d,但有些编译器会对其优化,使得d能够直接以1为参数构造,因此这里就不多做验证了。
那么对于多参的构造函数能否也转换呢?
在C++11以后就规定了多参构造函数也可以隐式类型转换了:
class E
{
public:
E(int x,int y)
:_x(x)
,_y(y)
{
}
private:
int _x;
int _y;
};
E e={1,1};
当然如果你不想发生这种类型转换,认为代码是严谨的,就可以用explicit来修饰构造函数,禁止类型转换:
class E
{
public:
explicit E(int x,int y)
:_x(x)
,_y(y)
{
}
private:
int _x;
int _y;
};
static成员
声明为static的类成员称为类的静态成员,用static修饰的成员变量,称之为静态成员变量;用static修饰的成员函数,称之为静态成员函数。静态成员变量一定要在类外进行初始化
使用场景有实现一个类,计算程序中创建出了多少个类对象。
class A
{
public:
A() { ++_scount; }
A(const A& t) { ++_scount; }
~A() { --_scount; }
static int GetACount() { return _scount; }
private:
static int _scount;
};
int A::_scount = 0;
void TestA()
{
cout << A::GetACount() << endl;//静态成员函数可以用作用域限定符调用
A a1, a2;
A a3(a1);
cout << A::GetACount() << endl;
}
特点:
- 静态成员为所有类对象所共享,不属于某个具体的对象,存放在静态区
- 静态成员变量必须在类外定义,定义时不添加static关键字,类中只是声明
- 类静态成员即可用 类名::静态成员 或者 对象.静态成员 来访问
- 静态成员函数没有隐藏的this指针,不能访问任何非静态成员
- 静态成员也是类的成员,受public、protected、private 访问限定符的限制
友元
在前面日期类的实现,我们已经浅谈过友元了。友元提供了一种突破封装的方式,有时提供了便利。但是友元会增加耦合度,破坏了封装,所以友元不宜多用。
友元函数:在类的任何一处用friend 函数声明,就可以使得这个函数在类外也可以调用类的保护、私有对象。
- 友元函数可访问类的私有和保护成员,但不是类的成员函数
- 友元函数不能用const修饰(因为const修饰的是this指针,而友元函数不是类的成员函数所以没有this指针)
- 友元函数可以在类定义的任何地方声明,不受类访问限定符限制
- 一个函数可以是多个类的友元函数
- 友元函数的调用与普通函数的调用原理相同
友元类
友元类的所有成员函数都可以是另一个类的友元函数,都可以访问另一个类中的非公有成员。
跟友元函数定义相差无几,有以下特点:
- 友元关系是单向的,不具有交换性。
- 友元关系不能传递
- 友元关系不能继承
啊! 这种关系让我想起了我的高等代数课,真是有种代数学的美啊。O.o
关于友元呢,大家对他的评价褒贬不一、两极分化。有人认为它破坏了类的封装是罪大恶极的,也有人觉得友元提供了一种不用写Get函数的便利。事实上任何事物都可以是一把双刃剑,就看你怎么使用罢了。就拿C++来说,有人拿他开发游戏、编写系统,也有人会用来开发非法软件、牟取暴利。所以,只要合适地使用友元,不刻意、恶意地破坏封装,我觉得问题是不大的。
内部类
如果一个类定义在另一个类的内部,这个内部类就叫做内部类。内部类是一个独立的类,它不属于外部类,更不能通过外部类的对象去访问内部类的成员。外部类对内部类没有任何优越的访问权限。
内部类就是外部类的友元类,但是外部类不是内部类的友元。
老实说,这多少有点抽象了。是不是像极了含辛苦茹养育孩子长大的父母,父母的东西属于孩子,但是孩子的东习…
emmmm。不得不提的是内部类的使用场景非常罕见,所以没必要过多纠结。
匿名对象
这里来的我目前所掌握的类和对象的最后一个知识点了。
正如c语言可以申请匿名结构体一样,我们的类也可以申请匿名对象。要注意好区别哦,是结构体匿名,但这里不是类匿名而是对象匿名哦!
class F
{
public:
F(int x=0)
:_x(x)
{
}
void Print()
{
cout<<Hehe<<endl;
}
private:
int _x;
};
int main()
{
F();
F(1);
F().Print;
return 0;
}
如上我们演示了匿名类的构造方式以及使用场景。
- 注意到,匿名对象可以F()形式构造,但有名对象不可以F f(),因为编译器会识别成函数声明
- 匿名对象的声明周期只有对应的那一行哦!
那么类和对象的内容就到此结束了,希望对读者有所帮助呢。