欢乐C++ —— 11. 类与对象
参考:
类是什么
从面向对象的角度来说,c++中的类是生成对象的模板。因为现实世界中很方便用对象来模拟,一个类就是描述同一类物品,而由该类生成的对象则是一个个实体。
从实现上讲,类封装了实现细节,而向外呈现方法。类也是属性和方法的集合。
类的编译过程
首先按顺序编译所有成员的声明,直到整个类可见后再编译成员函数体。
这块可以和类的组合嵌套中类的向前声明联系起来,在其类的声明之后定义之前,该类类型属于不完全类型,也就是说不清楚该类的有哪些成员,不知道要占用多少空间,所以此时定义该类的对象会出错,所以一个类的成员类型不能是自己。但这种不完全类型可以定义其指针与引用,并且可以用作函数的返回值类型与形参类型。 《c++ prime》 p250 类的声明
对象的创建与销毁过程
1. 分配内存空间
对于全局对象,静态对象以及分配在栈区域内的对象,对它们的内存分配是在编译阶段就规划好的。
而对于分配在堆区域内的对象,它们的分配是在程序运行阶段完成的。
2. 执行构造函数初始化列表
按照成员变量的从上到下声明顺序初始化成员变量。如果成员变量是类类型,则递归的去初始化它。
在成员变量初始化过程中,若初始化列表中指定了其初始化值,则用该初始值初始化它。否则该变量是未初始化的。类类型的成员变量,若初始值列表中没有指定其初始化方式则执行默认初始化。
引用,常量等都应该在构造函数初始值列表中初始化。
3. 执行构造函数体
当一个对象的初始化列表执行完毕,整个对象就算初始化成功了,构造函数体内所做的工作属于赋值。
构造函数是为已开辟内存空间的对象的成员变量初始化。
析构函数是为清除即将释放内存的对象所占用的外部资源。
4. 析构函数
无论何时,只要一个对象被销毁,就会自动调用析构函数。析构函数决定了在销毁一个对象前要干什么。
析构函数所做的工作就是释放对象所分配的外部资源,而不是释放对象本身所占的内存空间。对象本身,实际上是在析构函数体执行完后的隐含的析构阶段进行销毁。
静态成员
普通的成员变量是描述每个对象,而静态成员是描述一个类的。静态成员变量一个类只有一份,普通成员变量每个对象都有一份。
换句话说,如果银行的账户是一个类的话,那么不同人的账户就是不同的对象,每个人的存款不同,所以存款应该是普通的成员变量,而账户总数是描述银行账户这个类的,所以要设置成静态成员变量,相似的,利率对每个账户来说都是相同的,也应该设置成静态成员变量。(利率这个例子好像不太合适,利率不应该属于账户的属性,不过大概就这个意思)
相应的静态成员函数和静态成员变量的关系和普通成员函数和普通成员变量关系相似。
有时候使用类访问运算符,编译器分不清到底是使用类的静态成员变量还是类的内部类型,如果我们想使用后者的话,需要我们在使用时显式声明 typename
定义
在类外定义静态成员函数时不能重复static 关键字。
静态数据成员只能初始化一次。可以在类内通过给它限定 const 或 constexpr 从而类内初始化。即使类内初始化,也最好在类外定义一下。
this 指针
this 指针的实质是 classtype * const this;
即自身为常量的指向类类型的指针。
为什么会有this指针?
因为一个类定义的成员函数在内存中只有一份,被所有对象共用。而不同的对象的成员变量是相互独立的。为了让非静态成员函数操作调用它的对象,最方便起见就是为非静态成员函数添加一个指向调用对象的 this 指针。
this 指针如何使用?
所以当一个类经过c++编译器编译后,所有的非静态成员函数都会自动添加一个this指针形参,并且在非静态成员函数体内所有成员变量前自动加上 this->
,所有的非静态成员函数调用都会自动传入this实参。
Test a,*p = &a;
a.fun();
p->fun();
在调用时编译器会自动将 a的地址 和p保存的地址传给fun 中this 形参。
在函数内,我们也可以主动使用this 指针。
由this指针区分的三类成员函数
class Test{
void fun(); //普通成员函数
static void fun(); //静态成员函数 这块相同的函数不能重载静态和普通。但为了比较就先这样写
void fun() const; //常成员函数
}
- 普通成员函数, 有一个额外形参,类型及名称为
classtype * const this;
- 静态成员函数, 没有这个额外形参。
- 常成员函数, 有一个额外形参,类型及名称为
const classtype * const this;
这块有个成员函数互调的问题,其本质是const 指针的问题,简单说说就是
普通成员函数能不能和静态成员函数互相调用。
普通调静态可以。静态调普通不行,因为没有 this 指针,无法为普通传this实参。
普通成员函数能不能和常成员函数互调。
普通调常可以。常调普通不行,因为 const 指针赋值不兼容。具体可以搜const 指针赋值问题。
常成员函数能不能和静态成员函数互调。
常调静态可以。静态调常不行,因为没有 this 指针。
常成员函数
使用const 修饰成员函数来指出其是否由const 对象调用;
引用限定符
同样的可以使用引用限定符来指出一个成员函数是由右值还是左值调用。
不过,使用引用限定符的重载函数,其所有重载版本都要明确指出引用限定符。
![image-20200510171019021](https://img2020.cnblogs.com/blog/1512048/202007/1512048-20200706140407699-1350855597.png)
两种典型的类类型
这两种类主要区别就是拷贝和赋值时的操作不同。这块并不是非黑即白,有的类行为既不像值,又不像指针(例如IO类),而有的类具有两种特性。
行为像值的类
每个对象有自己的状态,不同对象相互的状态相互独立,当拷贝一个类值对象时,新对象和原对象是完全独立的。
行为像指针的类
每个对象共享状态,像引用计数一样,当拷贝一个类指针对象时,副本和原对象共用底层数据。
编写行为像指针的类可以借助 shared_ptr 的引用计数,其并不是说只能管理指针,要灵活运用它。
类的访问控制
public 公有,private 私有
实际上还有一种protected ,不过这个只有在继承中才与private 不同,所以放到后面继承再说
friend 友元声明可以无视这些限制,所有数据都可访问。
特殊的类
三种特殊的类:
- 嵌套类:一个类可以定义在另一个类的内部,这种类就叫做嵌套类。事实上,标准库容器的迭代器就是嵌套类。
- union 类:用来表示互斥的一类值的类
- 局部类:定义在函数体内部的类。
后面使用到再详细讨论。
成员指针
成员指针是指可以指向类的非静态成员的指针。以前我们学习的指针都是必须指向一个在内存中的实体。但是这个类成员指针有一点奇怪,它指向的更像是某一个成员类型。
下面是指向数据成员的成员指针。
class Test{
public:
int* data;
};
int main(){
int* Test::*pdata = &Test::data;
Test a,*p = &a;
a.*pdata = (int*)0x11116;
p->*pdata = (int*)0x11116;
cout<<a.data;
return 0;
}
成员函数指针与可调用对象
数据成员指针不常用,常用的是函数成员指针。
考虑这样一个场景,我想在一个 vector
vector<string> strs;
auto fp = &string::empty;
// find_if(strs.begin(),strs.end(),fp);//错误
/*
error: must use '.*' or '->*' to call pointer-to-member function
*/
//lambda
auto fun1 = [&](const string& str){return (str.*fp)();};
find_if(strs.begin(),strs.end(),fun1);
//函数对象
function<bool(const string&)> fun2 = fp;
find_if(strs.begin(),strs.end(),fun2);
fun2(strs[0]);
//使用<functional> 中的mem_fn 自动根据成员函数指针生成可调用的函数对象。
auto fun3 = mem_fn(fp);
find_if(strs.begin(),strs.end(),fun3);
fun3(strs[0]);
fun3(&strs[0]);
//借助 bind 函数
auto fun4 = bind(&string::empty,_1);
fun4(strs[0]);
fun4(&strs[0]);
find_if(strs.begin(),strs.end(),fun4);
理解第三行为什么错误很有必要。
find_if 利用迭代器 it 遍历序列并同时检测谓词(假设为fun) fun(*it)
是否为真。而此时fun 代表的是empty 函数它表面上并不接受任何参数,所以传给它一个 string 对象是错误的。而此处它的正确调用形式应为 (it->*fun)()
。
知道了错误原因后,想想办法如何改正。标准库的固定写法我们无法更改,我们只能给它传入 fun(*it)
形式的可调用对象。所以有以下几种方式:
-
我们可以借助 lambda 来主动改变调用形式。
-
可以借助函数对象,让编译器帮我们。
-
直接使用函数对象,使用 function<bool(const string &)> 。
由于标准库的调用形式是
fun(*it)
,即接受一个string
,可以写成const string &
。 -
隐式生成,使用 mem_fn 或 bind 。
mem_fn 和 bind 生成的可调用对象既可以接受string& 也可以接受 string* ,而function 生成的可调用对象却不行。这是为什么?
实际上,因为用function<bool(const string&)> 生成的可调用对象中,其函数调用运算符只有一个,接受 string & 类型,没有发生重载。而mem_fn 和 bind 返回的可调用对象中,函数调用运算符发生重载,既有接受string &,也有接受string *。
-