1. C++面向对象编程介绍
面向对象编程(Object Oriented Programming),简称OOP。
在传统的面向过程编程中,数据以及数据的相关操作函数都是分离的独立个体;
对象,如周围的一切其实都是对象;就编程角度,对象包括A)一系列属性(数据);B)一系列操作(函数)。
OOP提供了设计对象的功能,对象包括特性和行为,两者都囊括在一起,共同构成对象实体(即类实体);
对象实体,使程序更模块化,更易读易写,提升了代码重用到一个更高的层次;
对象提供了对数据操作的直接方法,定义了如何与对象进行交互,以及对象之间的交互;
更重要的是,OOP提供了更实用的概念:封装、继承、多态和抽象。
这篇主要讲述封装,即对象将数据及其操作函数封装到一个类实体中。
2. 类和类成员
C++提供了如char、int、long、float、double等基本数据类型,足够用来解决大多数哦相对简单的问题,但对于复杂的问题就比较困难了。
C++的一个实用特性是自定义数据类型;如之前的枚举和结构体:
枚举和结构体代表了传统的面向过程编程,它们只包含数据,如果需要访问此类型变量,必须提供自定义函数,并以该类型变量为参数:
在OOP世界中,我们希望自己的类型不仅包括数据,也包括操作数据的函数;C++中,通过class关键字来声明一个类。
2)类类似结构体,但它更多功能和更灵活:
类似结构体,类的声明没有分配内存,只是说明了类的结构;类的声明以分号结尾;
为了使用类,可以声明一个该类型的变量:
声明一个类的变量,也叫实例化一个类;该变量称作为类的一个实例,或者对象。
3)成员函数:
类不仅仅包含数据(成员变量),亦可以包含函数,称作成员函数:
成员函数可以类似访问成员变量,使用'.'来使用:
通常,类的成员变量,加一个前缀'm_'来区分:
3. 公有和私有
1)访问标识符:公有-public关键字,私有-private关键字
在上面的DateStruct的结构体,它的成员可以在main函数中访问,因为结构体的所有成员默认是公有的,即public;
公有成员在结构体和类中,程序的任何函数都可以访问;
但如果Date的成员没有public标识符,则会出现错误:
这个说明,类成员的默认是私有的,即private的;
私有的成员只能在类的定义之内的函数可以访问;
可以通过public关键字使成员变为公有的,即可访问了:
类和结构体的本质区别之一是类的成员可以使用标识符来控制它们的可访问性;
C++提供了3种不同的访问标识符:public、private、protect,分别是公有的、私有的和保护的:
4. 访问函数和封装
1)访问函数,也叫做读写函数;是读取和写入私有成员变量的值。
例如:
GetLength函数就是个访问函数。
访问函数有2种,即getter和setter:
将数据成员私有化,提供Getter和Setter来访问,即所谓的"封装" ;
2)封装
封装的思想就是将实现的细节隐藏,而暴露公有接口;
C++中的访问标识符,可以实现在类中的封装;通常是将所有的成员变量私有化;
尽管看起来访问成员变量的不直接,但使程序更有可重用性和可维护性;
A)封装实现,无论类的实现如何改变,只要对外的接口不发生变化即可。
如图上例,如果m_Value被重命名了,那么main函数中访问就会出错;
如果提供了m_Value的访问函数:
B)隐藏了类的实现,类的使用者只需知道公共的接口,就可以使用该类;
C)封装帮助防止意外的改变和误用;
D)对程序调试有很大的帮助,因为改变类的成员变量只用通过公共接口。
5. 构造函数Ⅰ
1)构造函数:是类的一种特殊的成员函数,当类被实例化时执行;通常用以初始化成员变量。
构造函数有明确的命名规则:A)函数名必须和类名一样;B)无返回类型(包括void)。
无参构造函数-不带参数的构造函数,是类的默认构造函数:
通常,类都包含一个默认的构造函数,可以初始化成员变量。
含参构造函数-含有参数的构造函数,可以对成员变量赋予指定的值;
以上的两个构造函数,类似重载函数;构造函数必须有唯一的前面(参数个数和参数类型)。
类亦可以只提供含参构造函数,没有默认构造函数:
6. 析构函数
析构函数是类的另一种特殊的函数,当类的对象销毁时调用;它和构造函数是成对出现的。
普通的简单类,一般不需要析构函数;因为C++会自动回收垃圾;
如果类中执行了某些动态内存分配,则需要显式定义析构函数,并释放回收垃圾;
析构函数的明确命名规则:A)函数名和类名一样,并前缀'~';B)不能带参数(即意味着只有一个析构函数);C)没有返回类型。
注意动态分配,必须提供析构函数,来回收分配的空间。
2)构造函数和析构函数的时序:
如上图所示,输出的依次是:simple的构造函数,pSimple的构造函数,最后是pSimple的析构函数。
构造函数和析构函数的时序是:Constructor First, Destructor Last。
7. 隐藏的'this'指针
如之前的例子中的this,是每个类的成员函数中隐藏的指针,它指向了类成员函数打交道的类的对象。
实用性:
1)当构造函数或成员函数中的参数名和成员变量名相同时,可以使用this来访问本类的成员变量;
2)可以使用this返回类的对象引用:
8. 构造函数Ⅱ
1)私有构造函数-如果不想类以外使用指定的构造函数,我们可以将它私有化。
类只能被实例化一次,称作为单一性;通常使用私有/保护构造函数进行的。
2)构造函数链和初始化
有时,一个构造函数所做的工作和另外的构造函数一样,只是增加了一些;
这样这个构造函数可以调用另外的构造函数,称作为构造函数链。如C#就支持这种格式,但C++不支持。
但构造函数可以调用类中的非构造函数,只是要注意这些非构造函数调用的成员,必须已经初始化。
通常的做法就是,定义一个公共的非构造函数,构造函数都调用它来初始化共同的;例如:
9. 类代码和头文件
1)在类的定义之外定义成员函数。
如之前的类定义,都是在类的定义中定义成员函数:
当类的定义越来越长和越来越复杂时,就显得臃肿,难以维护和操作;
幸运的是,C++提供了一种分离类的定义及其应用定义的方法,将类的成员函数在类的外面定义;格式是:类名::函数名
2)将类的定义放在头文件中:
头文件的使用可达到重用的效果;所以将类的定义放在头文件中,而成员函数放在.cpp中定义;而cpp的名字需和类的名字相同。
Date.h:
Date.cpp:
推荐分离类的定义中的成员函数到类外定义。
10. 常量类对象和成员函数
函数的参数可以为常量对象,如内置的基本数据类型一样,类对象也可以声明为常量,所有常量对象的变量必须在创建时初始化,其后不能修改。
上图3个错误,因为程序试图修改常量类对象的变量;
因为常量类对象不能调用非常量成员函数;
常量成员函数-保证不修改任何类变量或调用任何非常量函数。
为了使GetValue常量化,可以在其原型加个const关键字:
注意:
A) 常量成员函数在类外定义时,也必须加const关键字;
B) 任何常量成员函数试图修改类成员变量,或者调用非常量成员函数都是非法的,会产生编译错误。
C)构造函数不能常量化;
重载函数使用const和非const,是当返回类型不一样的时候。
11. 静态成员变量
在之前的程序中,静态表示变量的值在运行期间保持最新的值;
1)静态成员变量
在实例化两个对象,其包含的相同的成员变量;
静态成员变量是属于类的本身,是所有对象的共享变量;它的值是保持修改的最新值;
使用格式:类名::静态成员。
2)静态成员变量的初始化
初始化必须在类的代码文件中进行。
12. 静态成员函数
如静态成员变量一样,静态成员函数是属于类的本身,不属于任何类的对象;
如静态成员变量访问一样,可以通过:类名::静态成员函数来访问类的静态成员函数。
注意:静态成员函数,没有this指针;
例如:
13. 友元类和友元函数
多数时候,类和函数需要运行很紧密;但需使用显示函数来打印相关信息,这并不显得很隐藏类的细节;
这时,友元类和友元函数就可以很好的访问私有细节;
1)友元函数
友元函数访问类的私有成员,就如其是类的一个成员函数。
一个友元函数可以是,也可以不是其他类的成员函数;使用关键字friend。
例如:
一个友元函数可以是多个类的友元函数;
2)友元类
友元类是,声明在其他类中,可以访问其他类的私有变量;
在使用友元函数和友元类时,请务必谨慎。
14. 匿名变量和对象
所谓匿名,就是可以不通过命名变量来访问,减少临时变量。
【免责特此声明:
1)本内容可能是来自互联网的,或经过本人整理的,仅仅代表了互联网和个人的意见和看法!
2)本内容仅仅提供参考,任何参考该内容造成任何的后果,均与原创作者和本博客作者无关!】
封装
C++是一门集面向过程,面向对象以及泛型编程于一体的强大的编程语言,在这里面最重要的要属面向对象了吧???什么是面向对象?面向对象的思想是什么???总结下来就一句话:万物皆对象.在面向对象的世界里,一切都是可以用对象来解释的.这也是面向对象思想的精髓部分.
万物皆对象
举个例子:在超时里面进行商品购物的时候,它所有的商品都是按类目来划分的.生活用品,食品等.这就是按类别来分.而在自然界中也是.如:动物昆虫等.这就是类.将一类相似的的东西抽象成一类东西.
结构化程序设计
程序 = 算法 +数据结构
面向对象设计OOP
OBJECT ORIENRED PROGRAMING
Object oritened programing
程序 = 对象 +对象 +....
关键:让每个对象都负责执行一组相关的任务
面向对象开发范式的特性:
万物皆是对象
程序是一组对象彼此之间在发送消息
每个对象都有自己的内存占用,可以组装成更大的对象
每个对象都有类型,特定类型的对象都可以接收相同的消息
概念:
类:类是创建对象的模板和蓝图,类是一组相似对象的共同抽象定义
对象:对象是类的实例化结果,对象是实实在在的存在,代表现实世界的某一事物
对象的三大特性:
行为:对象能干什么
状态:对象的属性,行为的结果
标识:对象的唯一身份;
类和对象的区别:
类是静态定义
对象是动态实例
建立模型得到的是类而非对象
联系:
类是对象的定义
对象的产生离不开类这个模板
类存在的摸底是实例化得到对象
世界是由对象组成的
定义一个类的步骤:
1:定义类名
2:编写类的数据成员代表属性
3:编写类的方法代表行为
类的建模是一个抽象和封装的过程:
抽象:去掉不关注的,次要的信息而保留重要的信息
封装:信息打包
具体一点:将数据和行为结合在一个包中,对对象的使用者隐藏数据的具体实现方式
实现封装的关键:不能让类中的方法直接访问其他类的内部数据,只能通过公开行为方法间接访问:
例子:
class ClassName{
field1;
field2;
.....;
constructor;
.....
method1;
method2;
}
对象的两种类的形式:
1:结构体形式:
struct Saving{
unsigned accountNumber;
float balance;
};
缺点:安全性不好,任何人都是可以进行访问的
2:class形式:
class Savings{
public:
float deposit(float amount){
balance +=amount;
return balance;
}
private:
unsigned accountNumber;
float balance;
};
优点:类不仅可以保护数据,而且可以提供成员函数来操作
C++用类来定义抽象数据类型
C++早期的版本被成为带类的C
class 类名称{
public:
//共有函数
protected:
//保护成员
private:
//私有函数
//私有成员
int val;
};
类中定义成员函数:
class Tdate{
public:
void set(int m = 1,int d = 2,int y = 3){
month = m;
day = d;
year = y;
}
bool isLeepYear(){
return (year%4 ==0 &&year%100!=0) || (year %400==0);
}
void print(){
cout<
}
private:
int month,day,year;
}
调用:
int main(){
Tdate d;
d.set(2,4,1998);
d.print();
}
在类中定义成员函数:
类中定义的成员函数一般都为内联函数,即使没有明确用inline标示
在C++中,类定义通常在头文件中,因此这些成员函数定义也伴随这进入头文件
在类之后定义成员函数:
C++允许在其他地方定义成员函数;
将类定义和其成员函数定义分开,是目前开发程序的通常做法
我们把类定义看成是类的外部接口,类的成员函数定义看成是类的内部实现
看一个最简单的实例:在main.cpp中去定义一个类.并且在此类中直接实现成员函数.(也可以通过Car.h和Car.cpp的形式去实现)
/*
* ===========================================================================
*
* Filename: main.cpp
* Description:
* Version: 1.0
* Created: 2017年05月26日 22时24分05秒
* Revision: none
* Compiler: gcc
* Author: (),
* Company:
*
* ===========================================================================
*/
#include
#include
#include
using namespace::std;
class Car{
public:
void run(){
cout <<"car run"<
}
void shut(){
cout<<"car shutdown"<
}
void setProperty(int price,int carNum){
this->mPrice = price;
this->mCarNum = carNum;
}
private:
int mPrice;
int mCarNum;
};
int main(int argc,char *argv[]){
Car mCar;
/* *类的成员函数是不会占用内存的 */
cout <<"sizeof Car" <
cout << &mCar <
mCar.setProperty(10000,1000000);
mCar.run();
mCar.shut();
return 0;
}
通过Car.cpp及Car.h来实现一个简单类的封装
/*
* ===========================================================================
*
* Filename: Car.h
* Description:
* Version: 1.0
* Created: 2017年05月26日 22时43分53秒
* Revision: none
* Compiler: gcc
* Author: (),
* Company:
*
* ===========================================================================
*/
#ifndef __CAR_H__
#define __CAR_H__
#ifdef __cplusplus
extern "C"{
#endif
/* *
* 在头文件中去定义一个Car类型
* */
class Car{
private:
int mCarPrice;
int mCarNum;
int mCarType;
public:
void run();
void shut();
void setProperty(int price,int carNum,int carType);
void print();
};
#ifdef __cplusplus
}
#endif
#endif
/*
* ===========================================================================
*
* Filename: Car.cpp
*
* Version: 1.0
* Created: 2017年05月26日 22时48分41秒
* Revision: none
* Compiler: gcc
* Author: (),
* Company:
*
* ===========================================================================
*/
#include
using namespace::std;
/**
* 包含Car头文件
*/
#include
/* *
*类的行为的实现,定义
* */
void Car::run(){
cout << "car run" << endl;
}
void Car::shut(){
cout << "car shut" << endl;
}
void Car::setProperty(int price,int carnum,int cartype){
mCarPrice = price;
mCarNum = carnum;
mCarType = cartype;
if(mCarType == 1){
cout << "car type is one" <
}else if(mCarType == 2){
cout << "car type is two" <
}else if(mCarType == 3){
cout << "car type is three" <
}
}
void Car::print(){
cout <<"price:"<
cout <<"carnum:"<
cout <<"carType"<
}
/*
* ===========================================================================
*
* Filename: CarTest.cpp
* Description:
* Version: 1.0
* Created: 2017年05月26日 23时18分01秒
* Revision: none
* Compiler: gcc
* Author: (),
* Company:
*
* ===========================================================================
*/
#include
using namespace::std;
#include
/* *
*使用指针的方式来调用成员函数,类似结构体指针调用成员变量
* */
void usePoint(Car *mCar){
cout<<"==============="<
mCar ->setProperty(20000,20002,3);
mCar ->run();
mCar ->shut();
mCar ->print();
}
/* *
*使用引用的形式来调用成员函数,与对象的调用一致
* */
void userReference(Car &mCar){
cout << "==============="<
mCar.setProperty(10000,100001,2);
mCar.run();
mCar.shut();
mCar.print();
}
int main(int argc,char *argv[]){
Car mCar;
cout << "address:"<< &mCar <
cout <<"size of mCar"<< sizeof(mCar) <
mCar.setProperty(80001,88888,1);
mCar.run();
mCar.shut();
usePoint(&mCar);
userReference(mCar);
return 0;
}
以上是简单的关于类和对象以及一个简单的封装的国政
在成员函数中访问成员:
成员函数必须用对象来调用
Car mCar;
mCar.run();
在成员函数内部,访问数据成员或成员函数无需如此
void setProperty(int price,int carNum){
this->mPrice = price;
this->mCarNum = carNum;
}
this指针代表当前对象占用内存空间的地址:
void Tdate::set(int m ,int d,int y){
this->month = m;
this->day = d;
this->year = y;
}
通过指针来调用成员函数:
例子:
#include"tdate.h"
#include
void func(Tdate *pDate){
pDate->print();
if(pDate->isLeepYear()){
cout<<"leepyear"<
}else{
cout<<"not leepyear"<
}
}
通过引用来调用成员函数:
#include"tdate.h"
#include
void func(Tdate &pDate){
pDate.print();
if(pDate.isLeepYear()){
cout<<"leepyear"<
}else{
cout<<"not leepyear"<
}
}
类的成员函数的重载:
类的成员函数可以像普通函数一样进行重载
但是不同的类即使有相同的函数名也不算重载
类的成员函数可以默认设置成员参数:
在类之外去定义这样一个类的行为
OOP三大特性(面向对象的三个特性):
继承(inheritance):
多态(polymorphism):
封装(encapsulation):类背后隐藏的思想是数据抽象和封装
信息隐藏,隐藏对象的实现细节,不让外部直接访问
将数据成员和成员函数一起包装到 一个单元中 ,单元以类的形式实现
将数据成员和成员函数包装进类中,加上具体实现的隐藏,共同被称作封装,其结果是一个同时带有特征和行为的数据类型
封装类:定义类,定义其成员函数的过程称为封装类
细节:
除非必须公开底层的实现细节,否则应该将所有字段指定为private
使数据成员私有,控制数据访问限制.增强了类的可维护性
隐藏方法的具体实现.向外部提供公开的接口.以供安全调用
信息隐藏是OOP最重要的特性之一,也是可以使用访问修饰符的原因
访问修饰符号:
public
protected
private
信息隐藏的原因:
对模块的任何实现细节所做的更改不会影响使用该模块的代码
防止用户意外的修改数据
使模块易于使用和维护
C++类(Class)总结
一、C++类的定义
C++中使用关键字 class 来定义类, 其基本形式如下:
class 类名
{
public:
//行为或属性
protected:
//行为或属性
private:
//行为或属性
};
示例:
定义一个点(Point)类, 具有以下属性和方法:
■ 属性: x坐标, y坐标
■ 方法: 1.设置x,y的坐标值; 2.输出坐标的信息。
实现代码:
class Point
{
public:
void setPoint(int x, int y);
void printPoint();
private:
int xPos;
int yPos;
};
代码说明:
上段代码中定义了一个名为 Point 的类, 具有两个私密属性, int型的xPos和yPos, 分别用来表示x点和y点。
在方法上, setPoint 用来设置属性, 也就是 xPos 和 yPos 的值; printPoint 用来输出点的信息。
1 数据抽象和封装
抽象是通过特定的实例抽取共同特征以后形成概念的过程。一个对象是现实世界中一个实体的抽象,一个类是一组对象的抽象。
封装是将相关的概念组成一个单元,然后通过一个名称来引用它。面向对象封装是将数据和基于数据的操作封装成一个整体对象,对数据的访问或修改只能通过对象对外提供的接口进行。
2 类定义
几个重要名词:
(1) 类名
遵循一般的命名规则; 字母,数字和下划线组合,不要以数字开头。
(2) 类成员
类可以没有成员,也可以定义多个成员。成员可以是数据、函数或类型别名。所有的成员都必须在类的内部声明。
没有成员的类是空类,空类也占用空间。
class People
{
};
sizeof(People) = 1;
(3) 构造函数
构造函数是一个特殊的、与类同名的成员函数,用于给每个数据成员设置适当的初始值。
(4) 成员函数
成员函数必须在类内部声明,可以在类内部定义,也可以在类外部定义。如果在类内部定义,就默认是内联函数。
3 类定义补充
3.1 可使用类型别名来简化类
除了定义数据和函数成员之外,类还可以定义自己的局部类型名字。
使用类型别名有很多好处,它让复杂的类型名字变得简单明了、易于理解和使用,还有助于程序员清楚地知道使用该类型的真实目的。
class People
{
public:
typedef std::string phonenum; //电话号码类型
phonenum phonePub; //公开号码
private:
phonenum phonePri;//私人号码
};
3.2 成员函数可被重载
可以有多个重载成员函数,个数不限。
3.3 内联函数
有三种:
(1)直接在类内部定义。
(2)在类内部声明,加上inline关键字,在类外部定义。
(3)在类内部声明,在类外部定义,同时加上inline关键字。注意:此种情况下,内联函数的定义通常应该放在类定义的同一头文件中,而不是在源文件中。这是为了保证内联函数的定义在调用该函数的每个源文件中是可见的。
3.4 访问限制
public,private,protected 为属性/方法限制的关键字。
3.5 类的数据成员中不能使用 auto、extern和register等进行修饰, 也不能在定义时进行初始化
如 int xPos = 0; //错;
例外:
静态常量整型(包括char,bool)数据成员可以直接在类的定义体中进行初始化,例如:
static const int ia= 30;
4 类声明与类定义
4.1 类声明(declare)
class Screen;
在声明之后,定义之前,只知道Screen是一个类名,但不知道包含哪些成员。只能以有限方式使用它,不能定义该类型的对象,只能用于定义指向该类型的指针或引用,声明(不是定义)使用该类型作为形参类型或返回类型的函数。
void Test1(Screen& a){};
void Test1(Screen* a){};
4.2 类定义(define)
在创建类的对象之前,必须完整的定义该类,而不只是声明类。所以,类不能具有自身类型的数据成员,但可以包含指向本类的指针或引用。
class LinkScreen
{
public:
Screen window;
LinkScreen* next;
LinkScreen* prev;
}; //注意,分号不能丢
因为在类定义之后可以接一个对象定义列表,可类比内置类型,定义必须以分号结束:
class LinkScreen{ /* ... */ };
class LinkScreen{ /* ... */ } scr1,scr2;
5 类对象
定义类对象时,将为其分配存储空间。
Sales_item item; //编译器分配了足以容纳一个 Sales_item 对象的存储空间。item 指的就是那个存储空间。
6 隐含的 this 指针
成员函数具有一个附加的隐含形参,即 this指针,它由编译器隐含地定义。成员函数的函数体可以显式使用 this 指针。
6.1 何时使用 this 指针
当我们需要将一个对象作为整体引用而不是引用对象的一个成员时。最常见的情况是在这样的函数中使用 this:该函数返回对调用该函数的对象的引用。
class Screen
{
...
public:
Screen& set(char);
};
Screen& Screen::set(char c)
{
contents[cursor] = c;
return *this;
}
7 类作用域
每个类都定义了自己的作用域和唯一的类型。
类的作用域包括:类的内部(花括号之内), 定义在类外部的成员函数的参数表(小括号之内)和函数体(花括号之内)。
class Screen
{
//类的内部
...
};
//类的外部
char Screen::get(index r, index c) const
{
index row = r * width; // compute the row location
return contents[row + c]; // offset by c to fetch specified character
}
注意:成员函数的返回类型不一定在类作用域中。可通过 类名::来判断是否是类的作用域,::之前不属于类的作用域,::之后属于类的作用域。例如
Screen:: 之前的返回类型就不在类的作用域,Screen:: 之后的函数名开始到函数体都是类的作用域。
class Screen
{
public:
typedef std::string::size_type index;
index get_cursor() const;
};
Screen::index Screen::get_cursor() const //注意:index前面的Screen不能少
{
return cursor;
}
该函数的返回类型是 index,这是在 Screen 类内部定义的一个类型名。在类作用域之外使用,必须用完全限定的类型名 Screen::index 来指定所需要的 index 是在类 Screen 中定义的名字。
二 构造函数
构造函数是特殊的成员函数,用来保证每个对象的数据成员具有合适的初始值。
构造函数名字与类名相同,不能指定返回类型(也不能定义返回类型为void),可以有0-n个形参。
在创建类的对象时,编译器就运行一个构造函数。
1 构造函数可以重载
可以为一个类声明的构造函数的数量没有限制,只要每个构造函数的形参表是唯一的。
class Sales_item;
{
public:
Sales_item(const std::string&);
Sales_item(std::istream&);
Sales_item(); //默认构造函数
};
2 构造函数自动执行
只要创建该类型的一个对象,编译器就运行一个构造函数:
Sales_item item1("0-201-54848-8");
Sales_item *p = new Sales_item();
第一种情况下,运行接受一个 string 实参的构造函数,来初始化变量item1。
第二种情况下,动态分配一个新的 Sales_item 对象,通过运行默认构造函数初始化该对象。
3 构造函数初始化式
与其他函数一样,构造函数具有名字、形参表和函数体。
与其他函数不同的是,构造函数可以包含一个构造函数初始化列表:
Sales_item::Sales_item(const string &book): isbn(book), units_sold(0), revenue(0.0)
{ }
构造函数初始化列表以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个数据成员后面跟一个放在圆括号中的初始化式。
构造函数可以定义在类的内部或外部。构造函数初始化只在构造函数的定义中指定。
构造函数分两个阶段执行:(1)初始化阶段;(2)普通的计算阶段。初始化列表属于初始化阶段(1),构造函数函数体中的所有语句属于计算阶段(2)。
初始化列表比构造函数体先执行。不管成员是否在构造函数初始化列表中显式初始化,类类型的数据成员总是在初始化阶段初始化。
3.1 哪种类需要初始化式
const 对象或引用类型的对象,可以初始化,但不能对它们赋值,而且在开始执行构造函数的函数体之前要完成初始化。
初始化 const 或引用类型数据成员的唯一机会是构造函数初始化列表中,在构造函数函数体中对它们赋值不起作用。
没有默认构造函数的类类型的成员,以及 const 或引用类型的成员,必须在初始化列表中完成初始化。
class ConstRef
{
public:
ConstRef(int ii);
private:
int i;
const int ci;
int &ri;
};
ConstRef::ConstRef(int ii)
{
i = ii; // ok
ci = ii; // error
ri = i; //
}
应该这么初始化:
ConstRef::ConstRef(int ii): i(ii), ci(i), ri(ii) { }
3.2 成员初始化的次序
每个成员在构造函数初始化列表中只能指定一次。重复初始化,编译器一般会有提示。
成员被初始化的次序就是定义成员的次序,跟初始化列表中的顺序无关。
3.3 初始化式表达式
初始化式可以是任意表达式
Sales_item(const std::string &book, int cnt, double price): isbn(book), units_sold(cnt), revenue(cnt * price) { }
3.4 类类型的数据成员的初始化式
初始化类类型的成员时,要指定实参并传递给成员类型的一个构造函数,可以使用该类型的任意构造函数。
Sales_item(): isbn(10, '9'), units_sold(0), revenue(0.0) {}
3.5 类对象的数据成员的初始化
在类A的构造函数初始化列表中没有显式提及的每个成员,使用与初始化变量相同的规则来进行初始化。
类类型的数据成员,运行该类型的默认构造函数来初始化。
内置或复合类型的成员的初始值依赖于该类对象的作用域:在局部作用域中不被初始化,在全局作用域中被初始化为0。假设有一个类A,
class A
{
public:
int ia;
B b;
};
A类对象A a;不管a在局部作用域还是全局作用域,b使用B类的默认构造函数来初始化,ia的初始化取决于a的作用域,a在局部作用域,ia不被初始化,a在全局作用域,ia初始化0。
4 默认构造函数
不含形参的构造函数就是默认构造函数。
只要定义一个对象时没有提供初始化式,就使用默认构造函数。如: A a;
为所有形参提供默认实参的构造函数也定义了默认构造函数。例如:
class A
{
public:
A(int a=1,char c =''){}
private:
int ia;
char c1;
};
4.1 合成的默认构造函数
只有当一个类没有定义构造函数时,编译器才会自动生成一个默认构造函数。
一个类只要定义了一个构造函数,编译器也不会再生成默认构造函数。
建议:
如果定义了其他构造函数,也提供一个默认构造函数。
如果类包含内置或复合类型(如 int& 或 string*)的成员,它应该定义自己的构造函数来初始化这些成员。每个构造函数应该为每个内置或复合类型的成员提供初始化。
5 隐式类类型转换
5.1 只含单个形参的构造函数能够实现从形参类型到该类类型的一个隐式转换
class A
{
public:
A(int a)
{
ia =a;
}
bool EqualTo(const A& a)
{
return ia == a.ia;
}
private:
int ia;
};
A a(1);
bool bEq = false;
bEq = a.EqualTo(1);//参数为1,实现从int型到A的隐式转换
5.2抑制由构造函数定义的隐式转换
通过将构造函数声明为 explicit,来防止在需要隐式转换的上下文中使用构造函数:
class A
{
public:
explicit A(int a )
{
ia =a;
}
bool EqualTo(const A& a)
{
return ia == a.ia;
}
private:
int ia;
};
通常,除非有明显的理由想要定义隐式转换,否则,单形参构造函数应该为 explicit。将构造函数设置为 explicit 可以避免错误。
三 复制控制
1 复制构造函数
1.1 几个要点
(1) 复制构造函数
复制构造函数是一种特殊构造函数,只有1个形参,该形参(常用 const &修饰)是对该类类型的引用。
class Peopel
{
public:
Peopel();//默认构造函数
Peopel(const Peopel&);//复制构造函数
~Peopel();//析构函数
};
当定义一个新对象并用一个同类型的对象对它进行初始化时,将显式使用复制构造函数。
Peopel a1; Peopel a2 = a1;
当将该类型的对象传递给函数或函数返回该类型的对象时,将隐式使用复制构造函数。
Peopel Func(Peopel b){...}
(2)析构函数
析构函数是构造函数的互补:当对象超出作用域或动态分配的对象被删除时,将自动应用析构函数。
析构函数可用于释放构造对象时或在对象的生命期中所获取的资源。
不管类是否定义了自己的析构函数,编译器都自动执行类中非 static 数据成员的析构函数。
(3) 复制控制
复制构造函数、赋值操作符和析构函数总称为复制控制。编译器自动实现这些操作,但类也可以定义自己的版本。
(4) 两种初始化形式
C++ 支持两种初始化形式:直接初始化和复制初始化。直接初始化将初始化式放在圆括号中,复制初始化使用 = 符号。
对于内置类型,例如int, double等,直接初始化和复制初始化没有区别。
对于类类型:直接初始化直接调用与实参匹配的构造函数;复制初始化先使用指定构造函数创建一个临时对象,然后用复制构造函数将那个临时对象复制到正在创建的对象。直接初始化比复制初始化更快。
(5)形参和返回值
当形参或返回值为类类型时,由该类的复制构造函数进行复制。
(6)初始化容器元素
复制构造函数可用于初始化顺序容器中的元素。例如:
vector
编译器首先使用 string 默认构造函数创建一个临时值,然后使用复制构造函数将临时值复制到 svec 的每个元素。
(7)构造函数与数组元素
如果没有为类类型数组提供元素初始化式,则将用默认构造函数初始化每个元素。
如果使用常规的花括号括住的数组初始化列表来提供显式元素初始化式,则使用复制初始化来初始化每个元素。根据指定值创建适当类型的元素,然后用复制构造函数将该值复制到相应元素:
Sales_item primer_eds[] = { string("0-201-16487-6"),
string("0-201-54848-8"),
string("0-201-82470-1"),
Sales_item()
};
1.2 合成的复制构造函数
(1)合成的复制构造函数
如果没有定义复制构造函数,编译器就会为我们合成一个。
合成复制构造函数的行为是,执行逐个成员初始化,将新对象初始化为原对象的副本。
逐个成员初始化:合成复制构造函数直接复制内置类型成员的值,类类型成员使用该类的复制构造函数进行复制。
例外:如果一个类具有数组成员,则合成复制构造函数将复制数组。复制数组时合成复制构造函数将复制数组的每一个元素。
1.3 定义自己的复制构造函数
(1) 只包含类类型成员或内置类型(但不是指针类型)成员的类,无须显式地定义复制构造函数,也可以复制。
class Peopel
{
public:
std::string name;
unsigned int id;
unsigned int age;
std::string address;
};
(2) 有些类必须对复制对象时发生的事情加以控制。
例如,类有一个数据成员是指针,或者有成员表示在构造函数中分配的其他资源。而另一些类在创建新对象时必须做一些特定工作。这两种情况下,都必须定义自己的复制构造函数。
最好显式或隐式定义默认构造函数和复制构造函数。如果定义了复制构造函数,必须定义默认构造函数。
1.4 禁止复制
有些类需要完全禁止复制。例如,iostream 类就不允许复制。延伸:容器内元素不能为iostream
为了防止复制,类必须显式声明其复制构造函数为 private。
2 赋值操作符
与复制构造函数一样,如果类没有定义自己的赋值操作符,则编译器会合成一个。
(1)重载赋值操作符
Sales_item& operator=(const Sales_item &);
(2)合成赋值操作符
合成赋值操作符会逐个成员赋值:右操作数对象的每个成员赋值给左操作数对象的对应成员。除数组之外,每个成员用所属类型的常规方式进行赋值。对于数组,给每个数组元素赋值。
(3)复制和赋值常一起使用
一般而言,如果类需要复制构造函数,它也会需要赋值操作符。
3 析构函数
构造函数的用途之一是自动获取资源;与之相对的是,析构函数的用途之一是回收资源。除此之外,析构函数可以执行任意类设计者希望在该类对象的使用完毕之后执行的操作。
(1) 何时调用析构函数
(2)何时编写显式析构函数
如果类需要定义析构函数,则它也需要定义赋值操作符和复制构造函数,这个规则常称为三法则:如果类需要析构函数,则需要所有这三个复制控制成员。
(3)合成析构函数
合成析构函数按对象创建时的逆序撤销每个非 static 成员,因此,它按成员在类中声明次序的逆序撤销成员。
对于每个类类型的成员,合成析构函数调用该成员的析构函数来撤销对象。
合成析构函数并不删除指针成员所指向的对象。 所以,如果有指针成员,一定要定义自己的析构函数来删除指针。
析构函数与复制构造函数或赋值操作符之间的一个重要区别:即使我们编写了自己的析构函数,合成析构函数仍然运行。
四 友元
友元机制允许一个类将对其非公有成员的访问权授予指定的函数或类。
友元可以出现在类定义的内部的任何地方。
友元不是授予友元关系的那个类的成员,所以它们不受声明出现部分的访问控制影响。
建议:将友元声明成组地放在类定义的开始或结尾。
1 友元类
class Husband
{
public:
friend class Wife;
private:
double money;//钱是老公私有的,别人不能动,但老婆除外
};
class Wife
{
public:
void Consume(Husband& h)
{
h.money -= 10000;//老婆可以花老公的钱
}
};
Husband h;
Wife w;
w.Consume(h);
2 使其他类的成员函数成为友元
class Husband; //1.声明Husband
class Wife //2.定义Wife类
{
public:
void Consume(Husband& h);
};
class Husband //3.定义Husband类
{
public:
friend void Wife::Consume(Husband& h);//声明Consume函数。
private:
double money;//钱是老公私有的,别人不能动,但老婆除外
};
void Wife::Consume(Husband& h) //4.定义Consume函数。
{
h.money -= 10000;//老婆可以花老公的钱
}
注意类和函数的声明和定义的顺序:
(1)声明类Husband
(2)定义类Wife,声明Consume函数
(3)定义类Husband
(4)定义Consume函数。
五 static 类成员
static 成员,有全局对象的作用,但又不破坏封装。
1 static 成员变量
static 数据成员是与类关联的对象,并不与该类的对象相关联。
static 成员遵循正常的公有/私有访问规则。
2 使用 static 成员而不是全局对象有三个优点。
(1) static 成员的名字是在类的作用域中,因此可以避免与其他类的成员或全局对象名字冲突。
(2) 可以实施封装。static 成员可以是私有成员,而全局对象不可以。
(3) 通过阅读程序容易看出 static 成员是与特定类关联的,这种可见性可清晰地显示程序员的意图。
3 static 成员函数
在类的内部声明函数时需要添加static关键字,但是在类外部定义函数时就不需要了。
因为static 成员是类的组成部分但不是任何对象的组成部分,所以有以下几个特点:
1) static 函数没有 this 指针
2) static 成员函数不能被声明为 const (将成员函数声明为 const 就是承诺不会修改该函数所属的对象)
3) static 成员函数也不能被声明为虚函数
4 static 数据成员
static 数据成员可以声明为任意类型,可以是常量、引用、数组、类类型,等等。
static 数据成员必须在类定义体的外部定义(正好一次),并且应该在定义时进行初始化。
建议:定义在类的源文件中名,即与类的非内联函数的定义同一个文件中。注意,定义时也要带上类类型+"::"
double Account::interestRate = 0.035;
5 特殊的静态常量整型成员
静态常量整型数据成员可以直接在类的定义体中进行初始化,例如:
static const int period = 30;
当然char 可以转换成整形,也是可以的, static const char bkground = '#';
6 其他
(1)static 数据成员的类型可以是该成员所属的类类型。非 static 成员只能是自身类对象的指针或引用
class Screen
{
public:
// ...
private:
static Screen src1; // ok
Screen *src2; // ok
Screen src3; // error
};
(2)非 static 数据成员不能用作默认实参,static 数据成员可用作默认实参
class Screen
{
public:
Screen& clear(char = bkground);
private:
static const char bkground = '#';//static const整形变量可以在类内部初始化。
};
这段时间看了不少C++代码,也写了一个小项目,这篇文章来说一下我见到过的比较通用的两种多线程封装方式,实现平台为Linux
首先说说地一种线程封装方式,也是我们平常见得最多的一种封装方式,是用面向对象中的继承,多态来实现的,下面来看具体的代码,这些代码使我随手写的,主要是为了说明思想,如果要用到项目中还需要完善。
[cpp] view plain copy
这里我们用Thread这个类来实现线程的封装,用户自己要启用的线程类都要继承这个Thread类,在Thread类的Start函数里,调用了pthread_create创建了一个线程,并将ThreadFunc设置为线程函数,把线程的this指针传递给了这个函数,在这个线程函数里调用了
虚函数Run,这个Run函数最终会利用多态调用到用户线程类的Run函数,这就是上面代码的基本原理,比较简单,这也是面向对象用的比较多的地方,也可以说是他的强大的地方,不过用起来比较别扭,我们公司的关于线程的封装就是用的这种方法,个人感觉很麻烦,不好用,用户要想启用一个线程,必须要继承Thread这个类,而且要覆盖Run这个虚函数,很麻烦。
下面这种线程封装方式是用基于对象的封装方式,上面的封装方式是面向对象的,这种方式我们用到了boost库中的神器boost::function, boost::bind,这个神器原理就是帮定一个函数对象,函数对象可以包含参数,我们可以利用这个神器调用任意全局函数,甚至类的成员函数,
而不需要继承自任何类,下面来看具体代码
[cpp] view plain copy
我们可以看到Test类不需要继承自Thread类,我们直接可以将boost::bind帮定的函数对象传递给Thread的构造函数,这里为了简单我们把this指针传递给了ThreadFunc,我们完全可以传递任何参数给ThreadFunc, 因为我们不需要使用多态,这就是基于对象的思想,比面向对象似乎更直接,使用起来更方便,更简单。
C++11中的tr1中已经包含了boost这两大功能,boost确实是代表着最高水平的C++库,其中有很多东西值得学习,也有很多东西即将加入到标准C++库中。