C++ 类的基础操作函数

更新时间:2020-01-15 10:31:51点击次数:778次
C++ 类
C++相比于C语言,扩展的第一个特性就是其封装思想,或者说面向对象编程。封装思想是指对用户只知道其接口以及怎么使用这个对象,而无需知道其实现,而封装的实现方法就是类。而类,便是狭义的对象(广义的对象包括基本内置类型)。

class,struct
–C结构体扩展而来的类,this指针
我们可以通过写一个类来定义属于自己的复合类型(基于其他类型而定义的类型),C++的类相比C语言的结构体增加了成员函数。也就是说C++的类成员由 成员对象,成员函数 两者构成。这种扩展其实是通过一个传地址给this指针实现的。
在讲解this指针前我们先对一个类对象的大小进行sizeof运算,我们可以发现:对于一个空的类(不含任何成员),其大小为一个指针的大小,即一个字节。而对于一个非空的类,其大小等于所有成员变量的大小和。也就是说成员函数始终没有占据对象的内存。所以从根本上C++的类对象与C结构体的存储是没有区别的。
C++类的所有成员函数没有存储在类对象的内存,所有的对象共用同一成员函数。
那问题就出现了:既然成员函数不在对象的内存,那通过.运算符是如何访问到成员函数的?
当我们通过一个对象a以.运算符的方式调用成员函数时,实际上会进行的是将对象a的地址传给成员函数的第一个隐藏参数this指针。

a.print();
等价于:
A::print(&a);

this指针默认是一个指针常量,一经绑定就只能指向对象a。通过这种方式就能实现,成员函数不在对象内存,但是却可以逻辑上实现通过对象调取了成员函数。
伴随着this指针的存在,当我们在成员函数中调用一个对象成员变量名时,如果成员函数中没有单独声明这个同名变量,那么我们可以直接使用成员变量名,而无需再通过对象去调取成员变量。因为它会隐式的使用this指向的成员。

知识点:const成员函数

class A
{
 int f() const;
};
int A:: f() const
{}

我们可以通过以上形式声明一个const成员函数,需要注意的是const在声明和定义时都需要写,因为可以通过加const的方式重载成员函数。我们都知道重载是通过改写函数参数实现的。这里的重载实现方式也不例外。加了const的成员函数会将第一个隐式参数this指针的类型声明成指向常量的指针常量,也就是说在原本指针常量的基础上将其再附加成常量指针。所以不能在const成员函数内修改成员变量,因为this指针是一个常量指针!

类的成员函数
类的成员函数可以定义在类内部,也可以定义在类外部。定义在类外部,需要使用::域解析符来指出这个名字所属的作用域,定义在类内部的成员函数默认为内联函数。

构造函数与析构函数
构造函数与析构函数是每个类的最基本成员函数。构造函数负责对象成员的初始化,而析构函数负责对象销毁前的完善工作。
1.构造函数没有返回值类型
因为语法上默认为构造函数返回一个临时对象。注意void也不行,因为就算是void,我们还能使用return。
2.构造函数和析构函数都不允许被写成const成员函数。 error:构造函数或析构函数上不允许使用类型限定符
3.析构函数也没有返回值类型
当能够返回一个值的时候,我们就应该对这个值进行处理,但是若要处理这个值,就必须显式调用析构函数,这种做法显然是不正确的。
思考?
构造函数也可以显式调用,析构函数也可以显式调用,方法如下:

A a;
a.A::A();
a.A::~A();

那么为什么我们不要显式调用呢?
因为:
构造函数在对象生成时会自动隐式调用,如果再显式调用一次,如果隐式调用时,构造函数为对象分配了内存,当再次显式调用,之前那片内存就泄露了。同理,析构函数在对象生命周期结束之前被自动调用,如果构造函数中存在释放内存的操作,当再次显式调用,就会导致程序崩溃。虽然我们不应该通过一个对象显式调用构造函数与析构函数,但是我们可以直接调用构造函数得到一个临时对象。但是仍然不能直接调用析构函数!

类应包含的基本操作:
编译器能够帮忙合成的合成版本操作包括:
1.合成构造函数
2.合成析构函数
3.合成拷贝构造函数
4.合成拷贝赋值运算符
我们又将拷贝构造函数,拷贝赋值运算符,析构函数称为拷贝控制成员,因为当你需要写其中一者时,你就应该也重写其他两者。因为三者总是相互关联的。

构造函数之——初始化列表
因为在调用构造函数时进行初始化,当进入构造函数函数体的时候初始化就结束,所以如果我们的有些成员是const,引用,或者没有默认构造函数的类对象(需要提供参数初始化)时,就需要进行初始化,而不能依靠进入函数体的赋值。
例:

class A{
private:
int m;
int &n;
const int o;
public:
A(int _m,int _n,int _o):m(_m),n(_n),o(_o){
}
};
class B{
private:
int b;
A a;
public:
B(int _b,int _m,int _n,int _o):b(_b),a(_m,_n,_o){
}
};

注意;成员初始化的顺序与它定义的顺序有关,而与它在初始化列表中出现的的顺序无关:

int i;
int j;
A(int _a):j(_a),i(j){}
//例如在以上情况的时候,会先进行i的初始化,这时候就会导致出错。
//为避免以上错误的出现,我们应该尽量保证初始化的顺序与定义的顺序相同。
//而且应该尽量避免用一个成员去初始化另一个成员。

拷贝构造函数
定义:第一个参数是自身类型的引用,且其他任何参数都有默认值。
1.引用可以是常量引用,也可以是非常量引用。一般常写为常量引用。但必须是引用!因为在传参时会隐式调用拷贝构造函数,如果拷贝构造函数的参数不是引用,则会递归调用拷贝构造函数,这在逻辑上肯定时错误的。
2.拷贝构造函数不同于合成构造函数,当有任一非拷贝构造函数存在,这并不会影响编译器帮助合成拷贝构造函数,你只能通过自己编写拷贝构造函数才能够告诉编译器不需要合成拷贝构造函数。并且有时候你就算自己写了拷贝构造函数,编译器为了优化可能也会为你生成一个合成拷贝构造函数。

知识点: 拷贝初始化 与 直接初始化
我们需要注意的是:拷贝构造函数并不是只能用于拷贝初始化,拷贝初始化与直接初始化的区别在于是否调用一次类型转换,而与调用的构造函数类型无关。

string str(10,'.'); //直接初始化
string str = "peking university" //拷贝初始化  注意此时的=不是赋值的=,因为其是定义时初始化

直接初始化完成的步骤是:
直接找与传入参数相匹配的构造函数
拷贝初始化完成的步骤是:
如果就是自身类型,则直接调用拷贝构造函数,这个时候与直接初始化其实没有区别。如果不是自身类型,则先根据实参匹配一个构造函数,经过类型转换生成一个临时对象,再调用拷贝构造函数赋值。但这时调用的拷贝构造函数不是你自己写的拷贝构造函数,而是编译器合成版本,就算你自己写了,编译器也会帮你优化生成这个合成版本。

从上面步骤我们可以看出:直接初始化也可以调用拷贝构造函数,但是拷贝初始化一定会调用拷贝构造函数。另外,还有几个地方也一定会调用拷贝构造函数:①非引用对象作为形参②非引用对象作为函数返回值 这两种情况都是通过调用拷贝构造函数生成一个临时对象。

合成拷贝构造函数 --什么时候要自己写拷贝构造函数?
出现需要考虑浅拷贝和深拷贝的情况。编译器合成的拷贝构造函数的工作方式是逐位赋值,将除了static成员以外的所有成员逐位拷贝,对于对象则调用其拷贝构造函数。所以当构造函数中出现指针指向有分配的资源时,为避免拷贝构造函数调用后,两个指针指向同一片区域,应该自定义深拷贝构造函数。

Question:
为什么使用引用作为函数参数,或者返回值类型会提高程序运行效率?
因为省去了调用拷贝构造函数这一操作。

拷贝赋值运算符
重载运算符的本质是一个函数,例如拷贝赋值运算符其实是重载名为operator=的函数。
拷贝赋值运算符函数的返回值类型应该为左侧运算对象的引用。合成拷贝运算符的工作原理与合成拷贝构造函数一样,不过前者用于赋值,而后者是用于初始化,并且后者不具有返回值。

析构函数
1.定义一个析构函数
名字与类名相同,在前面加 ~ ,没有参数和返回值,一个类最多只能有一个析构函数。
(~ 是取反运算符,加上取反运算符是为了与构造函数所区别)。
2.析构函数的作用
析构函数在类对象生命期结束时自动被调用。析构函数用于对对象消亡前做善后工作。
(返回值如果是一个临时对象,那么这个临时对象的消亡是在临时对象存在的那句语句执行完之后消亡)
3.合成析构函数
如果没有定义析构函数,编译器生成析构函数,这个析构函数基本什么也不做
如果定义了析构函数,编译器就不合成析构函数。

疑问1:如何理解善后工作?
疑问2:为什么要自己编写析构函数
疑问3:为什么要编译器要生成析构函数?
疑问4:如何理解合成析构函数只做一些特定工作?

对于以上疑问的解答:
疑问1与疑问2:
析构函数的作用只是提供一个在对象删除前可以释放这个对象所占有的资源的机会。比如你在类中用new申请了一片内存,当类的生命期结束了,是不会释放堆中的内存。析构函数就为我们提供了一个在 清除指向这片区域的指针(如果清除了指针,就找不到那片空间了,就会导致生成内存碎片,或者说内存泄露)之前 用delete释放这片空间的机会。所以如果你的类只是一些基本内置类型,这时候其实析构函数什么也不做。注意:不是说类的成员的内存是在堆里面析构函数就会为你做什么,这还是需要你自己用delete释放。这是第一种有用的缺省析构函数。还有第二种就是,你的类是一个封闭类,这个封闭类中还含有另一个成员对象。当封闭类消亡时,析构函数会为你调用成员对象的析构函数。并且:将delete 写在析构函数中,编译器自动帮你调用,还能防止遗忘,导致内存泄露。
疑问3:
这是满足编译器的需求,满足标准,而并不是满足程序员的需要。就算缺省析构函数在你的情况下真的没有用,为满足编译,编译器也会为你生成。
注意:
就算你写了自己的析构函数,编译器也会为你合成一个析构函数。运行时,会先调用自定义析构函数,再调用合成的析构函数。
疑问4:
合成的析构函数只会做一件事:调用成员对象类或者基类对象的析构函数。继承体系中存在虚函数时,应该为你的类添写虚析构函数。

构造函数
构造函数的作用就是初始化成员。和析构函数一样,构造函数也被希望是隐式自动调用的,我们不应该通过对象再去显式调用构造函数。合成版本的构造函数也同样没做什么事情。他完成的工作就两个:一是调用其他成员对象或者基类的构造函数,二是满足编译器需要。但与析构函数不同的是,如果你自己写了任何构造函数,编译器就不再生成合成构造函数版本。

成员对象和封闭类的初始化
(重点内容)
Time a{
time b;
}
a是封闭类 b是成员对象。
1.任何生成封闭类对象的语句,都要让编译器明白,对象中的成员对象,是如何初始化的。
什么意思?如果成员对象的构造函数不是默认构造函数,是需要参数的。而编译器并不能知道参数是什么,且不能生成默认构造函数。就会导致编译失败。
具体做法就是:通过封闭类的构造函数初始化列表。

#include<iostream>
using namespace std;
class minute{
public:
int _minute; 
minute(int x):_minute(x){}
};
class hour{
public:
minute a;
int _hour;
public: hour(int x,int y):a(x),_hour(y){
}//在初始化列表中使用minute的构造函数 
};
int main(){
hour a(12,20);
cout << a._hour<<" "<< a.a._minute << endl; 

}

封闭类构造函数和析构函数的执行顺序:
1.封闭类对象生成时,先执行所有对象成员的构造函数,然后才执行封闭类的构造函数。

2.对象成员的构造函数调用次序和对象成员在类中的说明次序一致,与它们在成员初始化列表中出现的次序无关。

3.当封闭类的对象消亡时,先执行封闭类的析构函数,然后再执行成员对象的析构函数。次序和构造函数的调用次序相反。
总结:
先构造的后析构,后构造的先析构。
构造时先构造成员对象,再构造封闭类。
析构时先析构封闭类,再析构成员对象。

本站文章版权归原作者及原出处所有 。内容为作者个人观点, 并不代表本站赞同其观点和对其真实性负责,本站只提供参考并不构成任何投资及应用建议。本站是一个个人学习交流的平台,网站上部分文章为转载,并不用于任何商业目的,我们已经尽可能的对作者和来源进行了通告,但是能力有限或疏忽,造成漏登,请及时联系我们,我们将根据著作权人的要求,立即更正或者删除有关内容。本站拥有对此声明的最终解释权。

  • 项目经理 点击这里给我发消息
  • 项目经理 点击这里给我发消息
  • 项目经理 点击这里给我发消息
  • 项目经理 点击这里给我发消息