本篇博文对C++类和对象,以及特性做了一个总结,希望对初学C++的朋友有一点帮助。写的有点零碎,若有不当之处,欢迎拍砖。
1.抽象
面向对象方法中的抽象,是指对具体问题(对象)进行概括,抽出一类对象的公共性质并加以描述的过程。抽象分为数据抽象和行为抽象。
2.封装
封装就是将抽象得到的数据和行为(或功能)相结合,形成一个有机的整体,也就是将数据与操作数据的函数代码进行有机的结合,形成类,其中的数据和函数都是类的成员。
3.继承
C++语言提供了类的继承机制,允许程序员在保持原有类特性的基础上,进行更具体、更详细的说明。
4.多态
多态是指一段程序能够处理多种类型对象的能力。在C++语言中,这种多态可以通过强制多态、重载多态、类型参数化多态和包含多态四种形式来实现。虚函数是多态性的精华。模板是C++实现参数化多态性的工具,分为函数模板和类模板两种
5.类成员的访问控制
公有的(public)、私有的(private)和保护的(protected)
在类的外部只能访问类的公有成员,在类的成员函数中,可以访问到类的全部成员(包括当前类的其他对象的私有成员)。
6.对象所占据的内存空间只是用于存放数据成员,函数成员不在每一个对象中存储副本,每个函数的代码在内存中只占据一份空间。
7.内联成员函数
内联函数的声明有两种方式:隐式声明和显示声明。
将函数体直接放在类体内,这种方式称为隐式声明。显示声明则是将函数体置于类定义外,并在函数头前显示的加上inline关键字。
当然,普通函数也可以声明为内联函数,只需在函数定义前加inline关键字即可。
8.构造函数
构造函数的函数名与类名相同,而且没有返回值,构造函数通常被声明为公有函数。作为类的成员函数,构造函数可以直接访问类的所有数据成员,可以是内联函数,可以带有参数表,可以带默认的形参值,也可以重载。
如果没有创建构造函数,系统将默认创建一个函数体为空的无参构造函数(默认构造函数)。一旦创建了构造函数,系统将不会再提供默认构造函数。
创建对象时,如果调用的是默认构造函数(即没有参数的构造函数或所有参数都带默认值的构造函数),在对象创建时不要加括号,否则编译出错(当在后面用到该对象时)。
例如:
Point a();//编译器将该语句看做是函数的声明(返回值为Point类型的无参函数)
使用a对象;//error 编译出错
Point a;//Ok
构造函数的本质:
由于对象比较复杂,因此对象的定义并不像定义普通变量那么简单,但函数的功能却是非常强大,因此对象的定义就由一个叫做构造函数的东西来完成。
那么构造函数是如何完成这一工作的呢?
其实这和下面这个题目是一样的:
给一个数组,要求通过调用子函数给这个数组赋值。
构造函数也是采用了类似的方法,记住是类似并不完全相同。
看一个例子:
class A
{
public:
A()
{
a= 3;
b= 4;
}
private:
inta;
intb;
};
int main()
{
Aa;
return0;
}
查看汇编代码会看到
第17行的汇编代码,意思是把a这个对象的地址放到一个寄存器中,然后调用A类的构造函数A::A(),构造函数会从ecx中取得这个地址,然后通过这个地址给类的数据成员int a,和int b赋值。
在看我上面说的类似而不完全相同说的就是,当你调用子函数给数组赋值的时候你需要给子函数传递数组的首地址,但是调用构造函数却不用你去做这些操作,编译器替你完成了。
9.复制构造函数
其形参是本类对象的引用。其作用是使用一个已存在的对象(由复制构造函数的参数指定),去初始化同类的一个新对象。
如果没有创建复制构造函数,系统将自动创建一个默认的复制构造函数,该函数对对象的所有属性进行复制(浅拷贝)。一旦创建了复制构造函数,系统将不会再提供默认复制构造函数。一个类有且仅有一个复制构造函数。复制构造函数参数为该类对象的引用。
复制构造函数的参数为本类的引用,而不是本类的对象。因为如果复制构造函数的参数是本类的对象,在调用复制构造函数的时候,由于它的参数为本类的对象,还会调用复制构造函数的,这样就一直调用,永无止境了。但如果是引用的话就不会出现这种情况了。
普通构造函数是在对象创建的时候被调用,而复制构造函数在以下三种情况下会被调用。
①当用类的一个对象去初始化该类的另一个对象时。
Point a(1, 2);//创建一个新对象,普通构造函数被调用
Point b(a);//用对象a初始化对象b,复制构造函数被调用。
Point c = a; //用对象a初始化对象c,复制构造函数被调用。
以上对b和c的初始化都能够调用复制构造函数,两种写法只是形式上有所不同,执行的操作完全相同。
②如果函数的形参是类的对象,调用函数时,进行形参和实参结合时。
void print(Point p)
{
cout<< p.x << “,” << p.y << endl;
}
print(a);//函数形参为类的对象,当调用函数时,复制构造函数被调用。
只有把对象用值传递时,才会调用复制构造函数,如果传递引用,则不会调用复制构造函数。由于这一原因,传递比较大的对象时,传递引用比传值的效率高很多。
③如果函数的返回值是类的对象,函数执行完成返回调用者时。
如果返回类型为类的引用时,将不会调用复制构造函数。
此外,有些编译器会对复制构造函数的调用做优化,避免不必要的复制构成函数调用
类型转换构造函数
能够用一个参数调用的构造函数也称之为类型转换构造函数。
类型转换构造函数只能有一个参数,它不是复制构造函数。需要自己手写,缺省时,系统不会默认生成。
隐式的类型转换生成一个const类型的临时对象(只能使用const的引用类型来接受),显示的类型转换生成一个非const类型的临时对象(既可以使用const的引用类型来接受,也可以使用非const的引用类型来接受)。显示通过构造函数生成的临时对象都是一个非const类型的临时对象。
采用引用传递:
普通引用类型只能用非const对象或非const引用初始化,而const的引用类型既可以用非const对象或非const的引用初始化也可以用const对象或const的引用初始化。
如果采用值传递:
无论是普通类型还是const类型对象,既可以用const类型对象或const引用类型初始化也可以用普通类型或普通引用类型初始化。
10.析构函数
析构函数用来完成对象被删除前的一些清理工作,是在对象的生命期即将结束的时刻被自动调用的。析构函数不接受任何参数,但可以是虚函数。如果不进行显示说明,系统也会生成一个函数体为空的隐含析构函数。
11.类的组合
成员对象:一个类的成员变量是另一个类的对象
包含成员对象的类叫封闭类
类的组合描述的就是一个类内嵌其他类的对象作为成员的情况,它们之间的关系是一种包含与被包含的关系。当创建对象时,既要对本类的基本类型数据成员进行初始化,又要对内嵌对象成员进行初始化。
当创建对象时,先调用内嵌对象的构造函数(当有多个内嵌对象时,调用顺序按照内嵌对象在组合类的定义中出现的次序。内嵌对象在构造函数的初始化列表中出现的顺序与内嵌对象构造函数的调用顺序无关),再调用本类的构造函数。
有些数据成员的初始化,必须在构造函数的初始化列表中进行(如果自定义了复制构造函数,也必须要在复制构造函数的初始化列表中进行初始化):
①没有默认构造函数的内嵌对象(因为这类对象初始化时必须提供参数)
②引用类型的数据成员(因为引用类型变量必须在初始化时绑定引用的对象)
如果一个类包括这两类成员,那么编译器不能够为这个类提供隐含的默认构造函数,这时必须编写显示的构造函数,并且在每个构造函数(包括复制构造函数)的初始化列表中至少为这两类数据成员初始化。
析构函数的调用顺序与构造函数刚好相反。
初始化列表中的操作都是初始化。
由于要调用内嵌对象的构造函数(不属于上述两类的类)和析构函数,所以有时隐含的构造函数和析构函数并非什么也不做。
如果类的某个成员对象为引用类型,那么在构造函数中该成员对象对应的形参也必须是引用。因为如果是值传递,那么会调用成员对象类的复制构造函数生成成员对象类的一个临时对象,再用此临时对象初始化类的引用成员对象。但是类的构造函数调用之后,临时对象被析构,也就是类的引用成员对象引用的对象没有了,那么他的值自然是随机值了。
12.前向引用声明
当类A需要用到类B的类名(类B的定义又在类A的后面)时,必须在类A前先引用声明类B。在类A中,只能将类B的引用或指针作为数据成员,而不能将类B的对象作为类A的数据成员。在类A的隐式内联函数中不能使用类B的对象, 包括参数和返回值(即使是空函数体也不行),如果是类B的引用或指针则可以,但是在函数体中不能使用对象的任何成员(因为引用和指针都没有给对象分配空间);显示内联函数中则可以(前提是显示内联函数的实现是在类B的定义之后)。在类A中,函数的声明部分可以使用类B的类名(包括参数和返回值),但是函数的实现一定要置于类B定义之后。因为只能在类定义之后才能使用类的对象,才能为对象分配空间。
class B; //前向引用声明类B
class A
{
public:
inlinevoid fA1(B b); //OK 显示内联函数
voidfA2(B b){} //error 类A的隐式内联函数中不能使用类B的对象
voidfA3(B &b)//OK 但是在函数中不能使用类B的对象
{
//b.a;//error
}
BfA3(B b); //OK 函数声明中可以使用类B的对象
private:
B*b1; //OK 可以是类B的指针
B &b2; //OK 可以是类B的引用
B b3; //error 不可以是类B的对象
};
class B
{
public:
voidfB(A a)//类A已经定义过,可以使用类A的对象
{
}
voidprint()
{
cout<< " B print()" << endl;
}
private:
Aa; //类A已经定义过,可以使用类A的对象
};
inline void A::fA1(B b)//类A的显示内联函数可以使用类B的对象(在类B定义之后)
{
b.print();
}
B A::fA3(B b)
{
returnb;
}
13.结构体
结构体和类具有不同的默认访问控制属性:
在类中,对于未指定访问控制属性的成员,其访问控制属性为私有类型(private);在结构体中,对于未指定访问控制属性的成员,其访问控制属性为公有类型(public)。
14.联合体
联合体是一种特殊形态的类,它可以有自己的数据成员和函数成员,可以有自己的构造函数和析构函数,可以控制访问权限(默认访问控制属性是公有的)。联合体的全部数据成员共享同一组内存单元。
联合体自己可以有自定义的构造函数(普通构造函数、复制构造函数和构造函数的重载)和析构函数,但是联合体的各个对象成员,不能有自定义的构造函数(隐含的可以)、自定义的析构函数和重载的赋值运算符,不仅联合体的对象成员不能有这些函数,这些对象成员的对象成员也不能有,以此类推。联合体不能继承,因而也不支持包含多态。
一般只用联合体来存储一些公有的数据,而不为它定义函数成员。联合体也可以不声明名称(当联合体是全局(命名空间作用域)的时候,要在union前加static,否则编译出错。将带有union的命名空间置于头文件中。),成为无名联合体。无名联合体没有标记名,只是声明一个成员项的集合,这些成员项具有相同的内存地址,可以由成员项的名字直接访问。无名联合体通常用作类和结构体的内嵌成员。
例:
class B
{
B(); //error 类B是联合体的对象成员,不能有自定义的构造函数
~B(); //error 类B是联合体的对象成员,不能有自定义的析构函数
};
union A
{
A() //OK 联合体可以有构造函数
{
cout<< "A constructure A()" << endl;
}
A(inta):a(a) //OK 联合体可以有构造函数重载
{
cout<< "A constructure A(int a)" << endl;
}
A(charc):c(c) //OK 联合体可以有构造函数重载
{
cout<< "A constructure A(char c)" << endl;
}
~A() //OK 联合体可以有析构函数
{
cout<< "A destroy " << endl;
}
A(A&a) //OK 联合体可以有复制构造函数
{
b= a.b;
}
private:
inta;
charc;
Bb; // 类B是联合体的对象成员
};
15.位域
位域是一种允许将类中的多个数据成员打包,从而使不同成员可以共享相同的字节的机制。
在类定义中,位域的定义方式:
数据类型 成员名:位数;
使用位域,有以下几点需要注意:
①C++标准规定了使用这种机制用来允许编译器将不同的位域“打包”,但这种“打包”的的具体方式,C++标准并没有规定,因此不同的编译器会有不同的处理方式,不同编译器下,包含位域的类所占用的空间也会有所不同。
②只有bool、char、int和enum的成员才能够被定义为位域。
③位域虽然节省了内存空间,但由于打包和解包的过程中需要耗费额外的操作,所以运行时
间很有可能会增加。
结构体与类的唯一区别在于默认访问权限,因此也允许定义位域;但联合体中,各个成员本身就共用相同的内存单元,因此没必要也不允许定义位域。
16.类型转换构造函数
一个对象的类型转换,需要通过创建一个临时对象来完成。临时对象的生存期很短,在它所在的表达式被执行完后,就会被销毁。
一个构造函数,只要可以用一个参数调用,那么它就设定了一种从参数类型到这个类类型的类型转换。
在类定义中函数声明前加explicit关键字,就只允许显示的执行类型转换,对于隐式的转换则会报错。
如果函数的实现和函数在类定义中的声明是分离的,那么explicit关键字应当写在类定义中的函数原型声明处,而不能写在类定义外的函数实现处。因为explicit是用来约束这个构造函数被调用的方式的,属于一个类的对外接口的一部分,而是否加explicit关键字,与函数实现代码的生成无关。
注意:如果一个构造函数可以只用一个参数调用,并且由此定义的类型转换没有明确的说明,那么应当对这个构造函数使用explicit关键字,避免类型转换被误用。
17.对象作为函数参数和返回值的传递方式
对象作为函数参数时避免复制构造函数的调用:
①使用类的临时对象作为函数的参数
void print(Point p)
{
cout<<p.x << “,” << p.y << endl;
}
print(Point(1, 2));//创建Point类的临时对象作为函数参数,调用Point类的普通构造函数
②如果传参时发生由构造函数所定义的类型转换
print(1);//此时隐含的调用类型转换构造函数(假设存在可以用一个参数调用的构造函数)
③将函数参数定义为类的引用
void print(Point &p)
{
cout<<p.x << “,” << p.y << endl;
}
Point a(1, 2);
print(a);//用已存在的对象a初始化形参引用,只是取了个别名,没有调用构造函数
对象作为函数返回值时避免复制构造函数的调用:
Point& get1()
{
Pointa(1.0f, 2.0f);
return a ;//先构造对象,再将其返回。如果返回类型为引用时,return处不调用复制构
造函数,否则调用复制构造函数
}
Point Point::get2()
{
returnPoint(1.0f, 2.0f);// 直接将临时对象作为返回值。不管返回值是否为引用,只调用一次构造函数,return处将不再调用复制构造函数
}
Point a(1, 2);//调用一次构造函数
Point c = a.get(); //当get(get1或get2)的返回值是引用时,初始化时将调用复制构造函数,否则不调用
Point b;//调用一次无参构造函数
b = a.get(); // get(get1或get2)。该句为赋值语句,不管返回的是对象还是引用,都不调用复制构造函数,而是调用赋值运算符函数
对于上述情况,在不同的编译器上的结果可能不一样。但有一点,在函数调用时,如果函数参数为引用,传参时将不调用复制构造函数;如果函数返回值为引用,函数返回时将不调用复制构造函数。
18.规范
①类定义(置于“.h”文件中)和类实现(置于“.cpp”文件中)分开
②将使用到的头文件的包含都置于“.h”文件中,并在“.h”文件的开头加#pragma once,避免头文件被重复包含
③名空间的使用置于“.cpp”文件中
19.构造函数和析构函数调用的时期
class Demo
{
public:
Demo(intdata):m_data(data)
{
cout<< "Demo Construct " << m_data << endl;
}
~Demo()
{
cout<< "Demo Destruct " << m_data << endl;
}
private:
intm_data;
};
Demo d1(1); //globle 析构时,没有调用析构函数
int main()
{
Demod2(2); //in stack
Demo*pd = new Demo(4); //in heap
staticDemo d3(3); //local stack
deletepd; //手动释放时,才调用析构函数
return0;
}
运行结果:
Demo Construct 1
Demo Construct 2
Demo Construct 4
Demo Construct 3
Demo Destruct 4
Demo Destruct 2
Demo Destruct 3
Press any key to continue