参考书籍:21天学通C++
学习内容:
- 什么是类
- 类如何帮助整合数据和处理数据的方法(类似于函数)
- 构造函数、复制构造函数和析构函数
- C++11如何通过移动构造函数改进性能
- 封装和抽象等面向对象的概念
- this指针
- 结构是什么,它与类有何不同。
1. 类和对象
假设一个模拟人的程序。人有其特征:姓名、出生日期、出生地和性别,还能做某些事情,如交谈、自我介绍。前述特征是有关人的数据,而能做的事情是方法。如下图;
所以要模拟人,需要一个结构,将 定义人的属性(数据)以及人可使用这些属相执行的操作(类似于函数的方法)整合在一起,这种结果就是类。
1.1.1 声明类
声明类,用关键字class,一次包含类名、一组放在{}内的的成员属性和成员方法以及结尾的分号。声明类类似于函数声明,将类本身及其属性告诉编译器。类的声明本身并不改变程序的行为(除非你将类实例化成对象然后用它,就像调用函数一样)。
模拟人类的类如下L:
class Human
{
string Name;
string DateOfBirth;
string PlaceOfBirth;
string Gender;
void Talk(string TextToTalk);
void IntroduceSelf();
//...etc.
};
其中,IntroduceSelf()将使用Talk()以及整个类Human中的乙烯二数据。通过关键字class,C++提供了一种功能强大的方式,让您能够创建自己的数据类型,并在其中封装属性和使用他们的函数。类的所有属性(这里是Name、DateOfBrith、PlaceOfBirth和Gender)以及在其中声明的函数(Talk()和IntroduceSelf())都是类(Human)的成员.
封装指的是将数据及使用他们的方法进行逻辑编组,这是面向对象编程的重要特征。方法就是类成员的函数。
1.1.2 实例化对象
类相当于蓝图,仅声明类并不会对程序产生影响(上面也说了)。在运行阶段,对象时类的化身。要使用类的功能,通常需要根据类实例化一个对象,并通过对象访问成员方法和属性。实例化Human对象与创建其他类型(如double)的实例类似:
double Pi = 3.1415;
Human Tom;
就像为int动态分配内存一样,也可以使用new为Human对象动态地分配内存:
int* pNumber = new int;
delete pNumber;
Human* pAnotherHuman = new Human();
delete pAnotherHuman;
注意给类new动态分配内存时,类名后面需要加上小括号。
1.1.3 使用句点运算符访问成员
一个人的例子是Tom,男性,1970年出生于南极。Tom是Human类的实例化对象,是这个类存在于现实世界(运行阶段)的化身:
Human Tom;
类声明表示,Tom有DateOfBirth等属性,可以用句点运算符(.)来访问:
Tom.DateOfBirth = "1970";
这是因为从类声明表示的蓝图克制,属性DateOfBirth是Human的一部分。仅当实例化了一个对象之后,这个属性在显示世界(运行阶段)才存在。句点运算符(.)用于访问对象的属性。
这也适用于IntroduceSelf()等方法:
Tom.IntroduceSelf();
如果有一个指针pTom,它指向Human类的一个实例,则可使用指针运算符(->)来访问成员函数,也可以使用简介运算符(*)来获取对象,再使用句点运算符来访问成员:
Human* pTom = new Human();
(*pTom).IntroduceSelf();
1.1.4 使用指针运算符(->)访问成员
如果对象是使用new在自由存储区中实例化的,或者有指向对象的指针,则可以使用指针运算符(->)来访问成员属性和方法;
Human* pTom = new Human();
pTom->DateOfBirth = "1970";
pTom->IntroduceSelf();
delete pTom;
//或者下面的形式
Human Tom;
Human* pTom = &Tom;
pTom->DateOfBirth = "1970";
pTom->IntroduceSelf();
下面这个程序也是Human类,使用了关键private和public。
#include <iostream>
#include <string>
using namespace std;
class Human
{
private:
string Name;
int Age;
public:
void SetName(string HumanName)
{
Name =HumanName;
}
void SetAge(int HumanAge)
{
Age = HumanAge;
}
void IntroduceSelf()
{
cout<<"I am "+Name+" and am "\
<<Age<<" years old"<<endl;
}
};
int main()
{
Human FirstMan;
FirstMan.SetName("Adam");
FirstMan.SetAge(30);
Human FirstWoman;
FirstWoman.SetName("Eve");
FirstWoman.SetAge(28);
FirstMan.IntroduceSelf();
FirstWoman.IntroduceSelf();
return 0;
}
上面代码中的类Human包含了两个私有(private)变量,其中一个名为Name,类型为string,另一个名为Age,类型为int。还有几个使用私有变量的公有函数(也叫方法):SetName()、SetAge()、IntroduceSelf()。
1.2 关键字public和private
C++能够将雷属性和方法声明为公有的,这意味着有了对象以后就可获得它们;也可将其声明为私有的,这意味着这能在类的内部(或者其友元)中访问。作为设计者,我们可以使用C++关键字public和private来指定哪些部分可以从外部(如mian())访问,哪些部分不能。
将属性或者方法声明为私有的(private)有何优点呢?
代码段 小部件
假设有一个名为Eve的Human实例:
Human Eve;
如果试图用下述代码访问Eve的年龄:
cout<<Eve.Age;
将出现编译错误:“错误:Human::Age——不能访问Human类声明的私有成员”。要访问Age,唯一的途径就是通过Human类的公有方法GetAge(),这个方法可以编写Human类的程序员认为合适的方式暴露Age:
cout<<Eve.GetAge();
如果编写Human的程序员愿意,可以让GetAge()显示的年龄比Eve的实际年龄小。换句话说,可以决定要暴露哪些属性以及如何暴露。如果Human没有公有成员方法GetAge(),就可以确保用户根本无法查询Age。
同样,也不能直接给Human::Age赋值:
Eve.Age = 22;
要设置年龄,唯一的途径是通过SetAge():
Eve.SetAge(22);
这有很多优点。当前,SetAge()的实现只是直接设置成员变量Human::Age,但是也可以在SetAge()中验证外部输入,避免Age被设置成零或者负数;
class Human
{
private:
int Age;
public:
void SetAge(int InputAge)
{
if (InputAge>0)
{
Age = InputName
}
}
};
总之,C++让类的设计者能够控制类属性的访问和操纵方式。
1.2.1 使用关键字private实现数据抽象
C++可以使用关键字private指定哪些信息不能从外部访问(即在类外不可用),也可将方法声明为共有的(public),以便从外部通过这些方法访问私有信息。因此,类的实现可对外(其他类和main()等函数)隐藏您认为他们无需知道的内容。
回到Human类,其中Age是一个私有成员。要Human类向外支出的年龄比实际年龄小两岁很容易,只需在公有方法GetAge()中将Age减2再返回结果。
#include <iostream>
using namespace std;
class Human
{
privete:
int Age;
public:
void SetAge(int InputAge)
{
Age = InputAge;
}
int GetAge()
{
if(Age > 30)
{
return (Age-2);
}
else
{
return Age;
}
}
};
int main()
{
Human FirstMan;
FirstMamn.SetAge(35);
Human FirstWoman;
FirstWoman.SetAge(22);
cout<<"Age of FirstMan "<<FirstMan.GetAge()<<endl;
cout<<"Age of FirstWoman "<<FirstWoman.GetAge()<<endl;
return 0;
}
在面向对象的编程语言中,抽象是一个非常重要的概念,让程序员能够决定哪些属性只能让类及其成员知道,类外的任何人都不能访问(友元除外)。
1.3 构造函数
构造函数是一种特殊的函数(方法),在创建对象时被调用。与函数一样,构造函数也可以被重载。
1.3.1 声明和实现构造函数
构造函数是一种特殊的函数,它与类同名且不返回任何值。因此Human类的构造函数的声明类似于下面这样:
class Human
{
public:
Human();
}
这个构造函数可以再类生命中实现,也可以在类声明外实现。在类声明中实现(定义)构造函数的代码类似于下面这样:
class Human
{
public:
Human()
{
//constructor code here
}
};
在类声明外定义构造函数的代码实现如下:
class Human
{
public:
Human();//constructor declaration
};
Human::Human()
{
//constructor code here
}
其中::被称为作用于解析运算符。例如,Human::DateOfBirth指的是在Human类中声明的变量DateOfBirth,而::DateOfBirth表示全局作用域中的变量DateOfBirth。
1.3.2 何时及如何使用构造函数
构造函数总是在创建对象时被调用,这让构造函数是将类成员变量(int、指针等)初始化为已知值的理想场所。看下面一个例子:
#include <iostream>
#include <string>
using namespace std;
class Human
{
private:
string Name;
int Age;
public:
//constructor
Human()
{
Age = 0;
cout<<"Constructed an instance of class Human"<<endl;
}
void SetName(string HumansName)
{
Name = HumansName;
}
void SetAge(int HumansAge)
{
Age = HumansName;
}
void IntroduceSelf()
{
cout<<"I am "+Nmae+" and am "\
<<Age<<" years old"<<endl;
}
};
int main()
{
Human FirstMan;
FirstMan.SetName("Adam");
FirstMan.SetAge(30);
Human FirstWoman;
FirstWoman.SetName("Eve");
FirstWoman.SetAge(28);
FirstMan.IntroduceSelf();
FirstWoman.IntroduceSelf();
return 0;
}
可在不调用参数的情况下调用的构造函数被称为默认构造函数。默认构造函数是可选的。但是如果说没有提供构造函数,编译器将为程序员创建一个,会创建成员属性,但是不会初始化POD类型(如int)的属性。
1.3.3 重载构造函数
构造函数也可以重载,因此可以创建一个将姓名作为参数的构造函数,如下:
class Human
{
public:
Human()
{
//default constructor code here
}
Human(string HumansName)
{
//overload constructor code here
}
};
下面通过代码演示下重载函数的用途,它在创建Human对象时提供了姓名:
#include <iostream>
#include <string>
using namespace std;
class Human
{
private:
string Name;
int Age;
public:
Human()
{
Age = 0;
cout<<"Default constructor creates an instance of Human"<,endl;
}
Human(string HumansName)
{
Name = HumansName;
Age = 0;
cout<<"Overloaded constructor creates "<<Name<<endl;
}
Human(string HumansName,int HumansAge)
{
Name = HumansName;
Age = HumansAge;
cout<<"Overloaded constructor creates "\
<<Name<<" of "<<Age<<" years "<<endl;
}
void SetName(string HumansName)
{
Name = HumansName;
}
void SetAge(int HumansAge)
{
Age = HumansAge;
}
void IntroduceSelf()
{
cout<<"I am "+Name+" and am "\
<<Age<<" years old"<<endl;
}
};
int main()
{
Human FirstMan;
FirstMan.SetName("Adam");
FirstMan.SetAge(30);
Human FirstWoman("Eve");
FirstWoman.SetAge(28);
Huamn FirstChild("Rose",1);
FirstMan.IntroduceSelf();
FirstWoman.INtroduceSelf();
FirstChild.INtroduceSelf();
return 0;
}
Adam是用默认构造函数创建的;创建Eve时用第一个重载的构造函数,该构造函数接受一个string参数,并将其赋值给Human::Name;Rose是使用第二个重载函数创建的,该构造函数接受一个string参数和一个int参数,并将int参数赋值给Human::Age。
我们可以不识闲默认构造函数,而要求实例化对象时必须提供某些参数。
1.3.4 没有默认构造函数的类
下面是一个没有默认构造函数的类,要求创建Human对象时必须提供姓名和年龄。
#include <iostream>
#include <string>
using namespace std;
class Human
{
private:
int Age;
string Name;
public:
Human(string HumansName,int HumansAge)
{
Age = HumansAge;
Name = HumansName;
}
void IntroduceSelf()
{
cout<<"The name is "+Name+" and age is "<<Age<<endl;
}
};
int main()
{
cout<<"Please enter a name and a age"<<endl;
string NAME;
int AGE;
cin>>NAME>>AGE;
Human Eric(NAME,AGE);
Eric.IntroduceSeif();
return 0;
}
1.3.5 带默认值的构造函数
就像函数可以带默认参数一样,构造函数也可以。看下面的代码:
class Human
{
private:
string Name;
int Age;
public:
Human(string HumansAge,int HumansAge = 25)
{
Name = HumansName;
Age = HumansAge;
cout<<"Overload constructor creates "<<Name;
cout<<" of age "<<Age<<endl;
}
};
实例化这个类的对象时,可使用下面的语法:
Human Adam("Adam");
Human Eve("Eve",18);
注意,默认构造函数是调用时可以不提供参数的构造函数,而并不一定是不接受任何参数的构造函数。因此,下面的构造函数虽然有两个参数,但他们都有默认值,因此也是默认构造函数:
class Human
{
private:
string Name;
int Age;
public:
Human(string HumansName = "Adam",int HumansAge = 25)
{
Name = HumansName;
Age = HumansAge;
cout<<"Overloaded constructor creates "<<Name;
cout<<" of age "<<Age<<endl;
}
};
上面这个了类实例化时:
Human Adam;
1.3.6 包含初始化列表的构造函数
构造函数对初始化成员很有用。另一种初始化成员的方式是使用初始化列表。看下面的代码:
class Human
{
private:
string Name;
int Age;
public:
Human(string InputName,int InputAge)
:Name(InputName),Age(InputAge)
{
cout<<"Constructed a Human called "<<Name+", "<<Age<<" years old"<<endl;
}
};
初始化列表由包含在括号中的参数声明后面的冒号表示,冒号后面列出了各个成员变量及其初始值。初始值可以是参数(如InputName),也可以是固定值。使用特定参数调用基类的构造函数是,初始化俩表很有用。
下面的代码中Human类包含一个带初始化列表的默认构造函数,该默认构造函数的参数都有默认值。
#include <iostream>
#include <string>
using namespace std;
class Human
{
private:
int Age;
string Name;
public:
Human(string InputName = "Adam", int InputAge = 25)
:Name(InputName),Age(InputAge)
{
cout<<"Constructed a Human called "<<Name+<<", "<<Age<<" years old"<<endl;
}
};
int main()
{
Human FirstMan;
Huamn FIrstWoman("Eve",18);
return 0;
}
有默认值的初始化列表。
1.4 析构函数
与构造函数一样,析构函数也是一种特殊的函数。与构造函数不同的是,析构函数在对象销毁时自动被调用。
1.4.1 声明和实现析构函数
析构函数也看起来像一个与类同名的函数,但前面有一个波浪号(~)。因此,Human类的析构函数声明类似于下面这样:
class Human
{
~Human();
};
这个析构函数同样乐意在类声明中实现,也可以在类声明外实现。在类声明中实现(定义)析构函数的代码如下所示:
class Human
{
public:
~Human()
{
//destructor code here
}
};
在类声明外定义析构函数的代码类似于下面的:
class Human
{
public:
~Human();//destructor declaration
};
Human::~Human()
{
//destructor code here
}
析构函数的作用和构造函数的作用完全相反。
1.4.2 何时以及如何使用析构函数
每当对象不再在作用域内或通过delete被删除,进而被销毁时,都将启动析构函数。这使得析构函数是充值变量以及释放动态分配的内存和其他资源的理想场所。
在使用C风格的char缓冲区时,我们必须自己管理内存分配,不建议这么做。最好使用std::string。std::string等工具都是类,他们充分利用了构造函数和析构函数,还有与类相关的运算符。且先看下面的代码:
#include <iostream>
#include <cstring>
using namespace std;
class MyString
{
private:
char* Buffer;
public:
MyString(const char* InitialInput)
{
if(InitialInput!=NULL)
{
Buffer = new char[strlen(InitialInput)+1];
strcpy(Buffer,InitialInput);
}
else
{
Buffer = NULL;
}
}
~MyString()
{
cout<<"Invoking destructor, clearing up"<<endl;
if(Buffer!=Null)
{
delete[] Buffer;
}
}
int GetLength()
{
return strlen(Buffer);
}
const char* GetString()
{
return Buffer;
}
};
int main()
{
MyString SayHello("Hello from String Class");
cout<<"String buffer in MyString is "<<SayHello.GetLength();
cout<<" characters long"<<endl;
cout<<"Buffer contains: ";
cout<<"Buffer contains: "<<SayHello.GetString()<<endl;
return 0;
}
C风格字符串(MyString::Buffer),让您使用字符串是无需分配和释放内存。
注意:析构函数不能重载,每个类都只能有一个重载函数。如果忘记实现了重载函数,编译器将创建一个伪(dummy)析构函数并调用它。伪析构函数为空,即不释放动态分配的内存。
1.5 复制构造函数
1.5.1 浅复制及其存在的问题
先看下面的不稳定代码吧。
#include <iostream>
using namespace std;
class MyString
{
private:
char* Buffer;
public:
MyString(const char* InitialInput)
{
if(InitialInput != NULL)
{
Buffer = new char [strlen(INitialInput)+1];
strcpy(Buffer,InitialInput);
}
else
Buffer = NULL;
}
~MyString()
{
cout<<"Invoking destructor, clearing up"<<endl;
if (Buffer!=NULL)
delete[] Buffer;
}
int GetLength()
{
return strlen(Buffer);
}
const char* GetString()
{
return BUffer;
}
};
void UseMyString(MyString Input)
{
cout<<"String buffer in MyString is "<<Input.GetLength();
cout<<" characters long"<<endl;
cout<<"Buffer contains: "<<Input.GetString()<<endl
return ;
}
int main()
{
MyString SayHello("Hello from String Class");
UseMyString(SayHello);
return 0;
}
运行结果:
在上面的代码中,对象SayHello被复制到形参Input,并在UseMyString()中使用它。编译器之所以进行复制,是因为函数SayHello的参数Input被声明为按值(而不是引用)传递。对于整形、字符和原始指针等POD数据,编译器执行二进制复制,因此SayHello.Buffer包含的指针被复制到Input中,即SayHello.Buffer和Input.Buffer指向同一个内存单元,就像下面的图所示:
二进制复制并不深复制指向的内存单元,这导致两个MyString对象指向同一个内存单元。函数UseMyString()返回时,变量Input不在再作用域内,因此被销毁。因此,将调用MyString类的析构函数,这个析构函数使用delete释放分配给Buffer的内存。浙江导致main()中的对象SayHello指向的内存无效,而等mian()执行完毕时,SayHello将不再在作用域内,进而被销毁。然而。。。浅复制。
1.5.2 使用复制构造函数确保深复制
复制构造函数是一个特殊的重载构造函数,必须要有。每当对象被复制(包括将对象按值传递给函数)时,编译器都将调用复制构造函数。
为MyString类声明复制构造函数的语法如下:
class MyString
{
MyString(const MyString& CopySource);//copy constructor
};
MyString::MyString(const MyString& CopySource)
{
//copy constructor implementation code
}
复制构造函数接受一个以引用方式传入的当前类的对象作为参数。这个参数是源对象的别名,我们使用它来编写自定义的复制代码,确保对所有缓冲区进行深复制。
#include <iostream>
using namespace std;
class MyString
{
private:
char* Buffer;
public:
//constructor
MyString(const char* InitialInput)
{
cout<<"Constructor: creating new MyString"<<endl;
if(InitialInput!=NULL)
{
Buffer = new char[strlen(InitialInput)+1];
strcpy(Buffer,InitialInput);
cout<<"Buffer points to: 0x"<<hex<<(unsigned int*)Buffer<<endl;
}
else
Buffer = NULL;
}
//copy constructor
MyString(const MyString& CopySource)
{
cout<<"Copy constructor from MyString"<<endl;
if(CopySource.Buffer!=NULL)
{
//ensure deep copy by first allocating own buffer
Buffer = new char[strlen(CopySource.Buffer)+1];
strcpy(Buffer,CopySource);
cout<<"Buffer points to: 0x"<<hex<<(unsigned int*)Buffer<<endl;
}
else
Buffer = NULL;
}
~MyString()
{
cout<<"Invoking destructor,clearing up!"<<endl;
if (Buffer!=NULL)
delete Buffer;
}
int GetLength()
{
return strlen(Buffer);
}
const char* GetString()
{
return Buffer;
}
};
void UseMyString(MyString Input)
{
cout<<"String buffer in MyString is "<<Input.GetLength()<<" characters long."<<endl;
cout<<"Buffer contains: "<<Input.GetString()<<endl;
}
int mian()
{
MyString SayHello("Hello from string class");
UseMyString(SayHello);
return 0;
}
上面这个代码体现了深复制:
普通的赋值运算符 = 是将对象进行浅复制,这里还是容易出问题。所以有复制赋值运算符这么一说(深复制)。这个后面会详细说。
注意:复制构造函数中使用const,可确保复制构造函数不会修改指向的源对象。另外复制构造函数的参数必须按引用传递,否则它将复制实参的值,导致对源数据进行浅复制。
当类包含原始指针时,一定要编写复制构造函数和复制赋值运算符。编写复制构造函数时一定要将接受源对象的参数声明为const引用。
请尽量将类成员声明为std::string和智能指针,因为他们实现了复制构造函数,可以减少工作量。
1.5.3 有助于改善性能的移动构造函数
由于C++的特征和需求,有些情况下对象会被自动复制。看下面:
class MyString
{
//pick implementation from above
};
MyString Copy(MyString& Source)
{
MyString CopyForReturn(Source.GetString());
return CopyForReturn;
}
int main()
{
MyString sayHello("Hello World of C++");
MyString sayHelloAgain(Copy(sayHello));
return 0;
}
上面的代码调用了两次复制构造函数。一次是拷贝,一次是返回。如果动态数组对象很大,则会很影响效率。
所以还要写一个移动构造函数:
MyString(MyString&& MoveSource)
{
if(MoveSource.Buffer!=NULL)
{
Buffer = MoveSource.Buffer;
MoveSource.Buffer = NULL;
}
}
1.6 构造函数和析构函数的其他用途
1.6.1 不允许复制的类
如果要防止类对象被复制,可声明一个私有的复制构造函数。确保对象的复制不被编译通过,具体操作如下:
class President
{
private:
President(const President&);
President& operator= (const President&);
};
这里不需要给私有复制构造函数和私有赋值运算提供实现,只需将他们声明为私有就行了,可以确保类对象不被复制。
1.6.2 只能有一个实例的单例类
将关键字static用于类的数据成员时,该数据成员将在所有实例之间共享。
将static用于函数中声明的局部变量时,该变量的值将在两次调用之间不变。
static用于成员函数时,该方法将在所有成员之间共享。
#include <iostream>
#include <string>
using namespace std;
class President
{
private:
President(){};
President(const President&);
const President& operator=(const President&);
string Name;
public:
static President& GetInstance()
{
static President OnlyInstance;
return OnlyInstance;
}
string GetName()
{
return Name;
}
void SetName(string InputName)
{
Name = InputName;
}
};
int main()
{
President& OnlyPresident = President::GetInstance();
}
1.6.3 禁止在栈中实例化类。
将析构函数声明为私有即可。并用静态公有函数销毁实例,不这样做就会造成内存泄漏。
1.7 this指针
调用静态方法时,不会隐式调用this指针,因为静态函数不与实例相关联,而由所有实例共享。
如果要在静态函数中使用实例变量,应该显式的显示一个形参,让调用者将实参设置为this指针。
1.8 将sizeof()用于类
这里的局部变量不算在内。若有指针,与指针指向的数据量无关。
1.9 结构不同于类的地方
结构(C)默认都为公有,类默认都为私有,除非指定。此外结构以公有方式继承基结构,而类为私有继承。
继承在下个博客会学习。
1.10 声明友元
不能从外部访问私有数据和方法,但是友元类和友元函数例外。
关键字friend。