第九章:类和对象

从第九章开始主要讲一些与面向对象相关的概念。

一、类和对象的概念

开始前先举一个简单的例子,例如对人类这一生物建模,那么就需要指出人的一些属性和功能等,如下图所示:
在这里插入图片描述

1、类的声明

使用关键字class声明一个类,例如在上例中,为人类建模的类可以表示成如下所示的代码:

class Human 
{
	// Member attributes: 
	string name; 
	string dateOfBirth; 
	string placeOfBirth; 
	string gender;
	
	// Member functions:
	void Talk(string textToTalk); 
	void IntroduceSelf(); 
	...
};

可见,在C++中提供了一种创建自己数据类型的方法,该方法使用class关键字定义,可以封装自定义的属性和方法。
在上面的示例中,类Human中的属性有name, dateOfBirth, placeOfBirth, 和gender,声明的方法有Talk()和IntroduceSelf(),它们都是类Human的成员。
封装有助于帮助程序将某些数据和方法归类,是面向对象的重要特性。
常说的”方法“指的是类的成员之一,即类中的函数。

2、对象是类的实例

单独声明一个类,对程序的执行没有影响。在程序中,是以对象的形式来使用类。要使用类的功能,通常会创建该类的实例,称为对象。我们使用该对象来访问类的成员方法和属性。
创建类的对象,像创建数据类型的实例一样,如下所示:

double pi= 3.1415;   // a variable of type double
Human firstMan;  // firstMan: an object of class Human

或者,也可以使用new关键字动态创建实例,如下所示:

int* pointsToNum = new int; // an integer allocated dynamically 
delete pointsToNum; // de-allocating memory when done using

Human* firstWoman = new Human(); // dynamically allocated Human 
delete firstWoman; // de-allocating memory
3、使用点运算符访问类成员(.)

访问成员的方法如代码所示:

Human  firstMan;   // an instance i.e. object of Human
firstMan.dateOfBirth = "1970";  // 访问成员属性dateOfBirth

也可以使用下面的方式调用成员方法:

firstMan.IntroduceSelf();

如果有一个指向Human类实例的指针firstWoman,也可以使用指针运算符( -> )来访问成员,如下一节所述,或者使用间接运算符(*)来引用点后面的对象。

Human* firstWoman = new Human(); 
(*firstWoman).IntroduceSelf();
4、使用指针操作符( -> )访问类成员

如果使用new关键字实例化一个对象,或者有一个指针指向一个对象,那么可以使用操作符( -> )来访问类成员:

Human* firstWoman = new Human(); 
firstWoman->dateOfBirth = "1970"; 
firstWoman->IntroduceSelf(); 
delete firstWoman;

代码示例:

#include <iostream>
#include <string>
using namespace std;

class Human
{
public:
	string name;
	int age;

	void IntroduceSelf()
	{
		cout << "I am" + name << " and am ";
		cout << age << " year old " << endl;
	}
};

int main()
{
	// An object of class Human with attribute name as "Adam"
	Human firstMan;
	firstMan.name = "Adam";
	firstMan.age = 30;

	// An object of class Human with attribute name as "Eve"
	Human firstWoman;
	firstWoman.name = "Eve";
	firstWoman.age = 28;

	firstMan.IntroduceSelf();
	firstWoman.IntroduceSelf();

	return 0;
}

输出:
I amAdam and am 30 year old
I amEve and am 28 year old

二、关键字public和private

可以将类中成员声明为public类型或private类型,类的对象可以访问声明为public的成员,声明为private的成员只能在类内部访问。
这样可以防止类对象直接更改类的属性,造成某些不必要的错误(比如可能改属性不能小于0)。

1、关键字private

关键字private确保类中某些信息无法被外部成员访问,但是可以通过声明为public的成员间接对private数据进行访问。代码示例如下所示:

#include <iostream>
using namespace std;

class Human
{
private:
	// Private member data:
	int age;

public:
	void SetAge(int inputAge)
	{
		age = inputAge;
	}
	// Human lies about his / her age (if over 30)
	int GetAge()
	{
		if (age > 30)
			return (age - 2);
		else
			return age;
	}
};

int main()
{
	Human firstMan;
	firstMan.SetAge(35);

	Human firstWoman;
	firstWoman.SetAge(22);
	
	cout << "Age of firstMan " << firstMan.GetAge() << endl;
	cout << "Age of firstWoman " << firstWoman.GetAge() << endl;

	return 0;
}

输出:
Age of firstMan 33
Age of firstWoman 22

上面代码中,类Human的成员属性age为private类型,无法直接访问,但是可以通过SetAge和GetAge两个方法来访问。使用这种方式的好处是,外部用户无法直接对属性age做更改,类可以在方法中控制age的值。

这里实现了一种抽象的概念,抽象是面向对象语言中的一个重要概念。 它使程序员能够决定只有类及其成员才能知道类的那些属性,除了那些被称为“朋友”的人之外,没有人可以访问它。

三、构造函数

构造函数是在类的实例化期间调用以构造对象的特殊函数(或方法),像函数一样,构造函数也可以重载。

1、构造函数的声明和实现

构造函数是一个特殊的函数,它和类具有相同的名称,并且不返回任何值。构造函数可以在类内部实现,也可以在类外部实现。
在类内部实现的情况:

class Human 
{ 
public: 
	Human()
	{
		// constructor code here
	} 
};

在类外部实现的情况:

class Human 
{ 
public:
	Human(); // constructor declaration 
};

// constructor implementation (definition) 
Human::Human() 
{ 
	// constructor code here 
}

需要注意的是,两个冒号连在一起的这个符号(::)称为范围解析运算符(scope resolution operator)。例如,Human::dateOfBirth是指在类Human范围内声明的变量dateOfBirth。而::dateOfBirth,指的是全局范围内的另一个变量dateOfBirth。

2、构造函数的使用

实例化一个类时,默认调用构造函数,因此可以将类的成员变量(如整数,指针等)进行初始化。代码示例如下:

#include <iostream>
#include <string>
using namespace std;

class Human
{
private:
	string name;
	int age;

public:
	Human() // constructor
	{
		age = 1;  // initialization
		cout << "Constructed an instance of class Human" << endl;
	}

	void SerName(string humansName)
	{
		name = humansName;
	}
	void SetAge(int humasAge)
	{
		age = humasAge;
	}
	void IntroduceSelf()
	{
		cout << "I am " + name << " and am ";
		cout << age << " years old" << endl;
	}
};

int main()
{
	Human firstWoman;;
	firstWoman.SerName("Eve");
	firstWoman.SetAge(28);
	firstWoman.IntroduceSelf();

	return 0;
}

输出:
Constructed an instance of class Human
I am Eve and am 28 years old

需要注意的是,不带参数的构造函数称为默认构造函数。
如果没有编写任何构造函数,编译器会默认创建一个构造函数(也会创建成员属性,但不会将其初始化为任何特定的非零值)。

3、构造函数重载

构造函数可以像函数一样重载,代码示例如下所示:

#include <iostream>
#include <string>
using namespace std;

class Human
{
private:
	string name;
	int age;

public:
	Human() // default constructor
	{
		age = 0;  // initialization to ensure no junk value
		cout << "Default constructor: name and age not set" << endl;
	}
	Human(string humansName, int humansAge)  // overloaded
	{
		name = humansName;
		age = humansAge;
		cout << "Overloaded constructor creates ";
		cout << name << " of " << age << " years " << endl;
	}
};

int main()
{
	Human firstMan;  // use default constructor
	Human firstWoman("Eve", 20);  // use overloaded constructor

	return 0;
}

输出:
Default constructor: name and age not set
Overloaded constructor creates Eve of 20 years

4、没有默认构造函数的类

下面的代码中,没有默认构造函数。当存在重载的构造函数是,编译器不会自己添加默认构造函数。所以,由于重载函数的存在,实例化Human类时必须带上参数humansName和humansAge。
代码示例:

#include <iostream>
#include <string>
using namespace std;

class Human
{
private:
	string name;
	int age;

public:
	Human(string humansName, int humansAge)  
	{
		name = humansName;
		age = humansAge;
		cout << "Overloaded constructor creates ";
		cout << name << " of " << age << " years " << endl;
	}

	void IntroductionSelf()
	{
		cout << "I am " + name << " and am ";
		cout << age << " years old " << endl;
	}
};

int main()
{
	Human firstMan("Adam", 25);  
	Human firstWoman("Eve", 28);

	firstMan.IntroductionSelf();
	firstWoman.IntroductionSelf();

	return 0;
}

输出:
Overloaded constructor creates Adam of 25 years
Overloaded constructor creates Eve of 28 years
I am Adam and am 25 years old
I am Eve and am 28 years old

5、具有默认值的构造函数参数

构造函数也可以像函数一样指定参数的默认值。如下列代码所示:

class Human 
{ 
private:
	string name; 
	int age;
public:
	// overloaded constructor (no default constructor) 
	Human(string humansName, int humansAge = 25) 
	{
		name = humansName;
		age = humansAge;
		cout << "Overloaded constructor creates " << name;
		cout << " of age " << age << endl; 
	}
	// ... other members
};

需要注意的是,默认构造函数是可以在没有参数的情况下实例化的构造函数,而不一定是不带参数的构造函数。因此,具有两个参数(具有默认值)的下列构造函数是默认构造函数:

class Human 
{ 
private:
	string name; 
	int age;
public:
	// default values for both parameters
	Human(string humansName = "Adam", int humansAge = 25)
	{
		name = humansName;
		age = humansAge;
		cout << "Overloaded constructor creates " << name;
		cout << " of age " << age << endl; 
	}
	// ... other members
};

原因是类Human仍然可以在没有参数的情况下实例化:
Human adam; // Human takes default name “Adam”, age 25

6、具有初始化列表的构造函数

上面以及了解到构造函数能够初始化成员变量,初始化成员的另一种方法是使用初始化列表。
具体如下代码所示:

#include <iostream>
#include <string>
using namespace std;

class Human
{
private:
	string name;
	int age;

public:
	Human(string humansName = "Adam", int humansAge = 25)
		:name(humansName), age(humansAge)
	{
		cout << "Constructed a human called " << name;
		cout << ", " << age << " years old" << endl;
	}
};

int main()
{
	Human adam;
	Human eve("Eve", 28);

	return 0;
}

输出:
Constructed a human called Adam, 25 years old
Constructed a human called Eve, 28 years old

四、析构函数

析构函数和构造函数一样,也是一个特殊的函数。在对象实例化时调用构造函数,并在销毁对象时自动调用析构函数。

1、析构函数的声明和实现

上面的示例中Human函数的析构函数,其声明如下所示:

class Human 
{
	~Human(); // declaration of a destructor 
};

和构造函数一样,析构函数的声明也可以在类内部或类外部。
析构函数的声明和构造函数几乎一样,仅一个符号(~)的差别,但是功能却完全相反。

2、析构函数的使用

当类的对象超出范围或通过delete关键字删除时,总是会调用析构函数。此属性使析构函数成为重置变量并释放动态分配的内存和其他资源的理想位置。代码示例如下:

#include <iostream>
#include <string.h>
using namespace std;

class MyString
{
private:
	char* buffer;

public:
	MyString(const char* initString)  // constructor
	{
		if (initString != NULL)
		{
			buffer = new char[strlen(initString) + 1];
			strcpy_s(buffer, strlen(initString) + 1, initString);
		}
		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 sayHello is " << sayHello.GetLength();
	cout << " characters long" << endl;

	cout << "Buffer contains: " << sayHello.GetString() << endl;
}

输出:
String buffer in sayHello is 23 characters long
Buffer contains: Hello from String Class
Invoking destructor, clearing up

构造函数通过强制输入参数强制构造带有输入字符串,然后使用new和strlen为它分配内存后将其复制到字符缓冲区。strlen是标准库提供的函数,用于求字符串长度。 析构函数代码确保在构造函数中分配的内存自动返回到系统。它检查MyString :: buffer是否不为NULL,如果是,则在它上面执行delete [],释放构造函数中使用关键字new分配的内存空间。

需要注意的是,一个类只能有一个析构函数。如果忘记实现析构函数,编译器会默认创建并调用一个虚拟析构函数,即一个空的析构函数(不会清除动态分配的内存)。

五、拷贝构造函数

对于普通类型的数据,赋值操作实际是一种复制操作,如下面的代码,将a的值复制给b:
int a = 100;
int b = a;

类的赋值操作稍有不同,如下面的代码,赋值操作不是直接赋值,而是调用拷贝构造函数来完成复制。
CExample A(100);
CExample B = A; //注意这里的对象初始化要调用拷贝构造函数,而非赋值

拷贝构造函数的工作过程如下代码所示:

#include <iostream>
using namespace std;

class CExample {
private:
    int a;
public:
    //构造函数
    CExample(int b)
    { a = b;}
    
    //拷贝构造函数
    CExample(const CExample& C)
    {
        a = C.a;
    }

    //一般函数
    void Show ()
    {
        cout<<a<<endl;
    }
};

int main()
{
    CExample A(100);
    CExample B = A; // CExample B(A); 也是一样的
     B.Show ();
    return 0;
} 

CExample(const CExample& C) 就是我们自定义的拷贝构造函数。可见,拷贝构造函数是一种特殊的构造函数,函数的名称必须和类名称一致,它必须的一个参数是本类型的一个引用变量。

1、浅层拷贝

在上一小节的代码示例中,类MyString的参数是一个指针成员,指向动态分配的内存,在构造函数中使用new分配,并在析构函数中使用delete []解除分配。这种情况下传递参数,将复制指针成员,但不会复制指向的内存,从而导致两个对象指向内存中相同的动态分配的缓冲区。当一个对象被破坏时,delete []释放内存,从而使另一个对象持有的指针无效。 这种复制只复制类指针成员,属于浅层复制,会对程序的稳定性构成威胁。代码示例如下所示:

#include <iostream>
#include <string.h>
using namespace std;

class MyString
{
private:
	char* buffer;

public:
	MyString(const char* initString)  // constructor
	{
		buffer = NULL;
		if (initString != NULL)
		{
			buffer = new char[strlen(initString) + 1];
			strcpy_s(buffer, strlen(initString) + 1, initString);
		}
		else
			buffer = NULL;
	}

	~MyString()  // Destructor
	{
		cout << "Invoking destructor, clearing up" << endl;
		delete[] buffer;
	}

	int GetLength()
	{
		return strlen(buffer);
	}
	const char* GetString()
	{
		return buffer;
	}
};

void UseMyString(MyString str) {
	cout << "String buffer in MyString is " << str.GetLength();
	cout << " charactors long" << endl;
	cout << "buffer contains: " << str.GetString() << endl;
	return;
}

int main()
{
	MyString sayHello("Hello from String Class");
	UseMyString(sayHello);

	return 0;
}

输出:
String buffer in MyString is 23 charactors long
buffer contains: Hello from String Class
Invoking destructor, clearing up
Invoking destructor, clearing up

上述代码输出以上内容后,程序无法正常退出。分析原因可知,MyString实例化一个对象sayHello时,会在构造函数中声明一个指针,指向内存中的某个内存,然后将其赋值给函数UseMyString,此时有两个指针指向同一个内存,当UseMyString函数执行完之后,调用析构函数,内存被释放,当main函数结束之后,对象sayHello执行完成,也要调用析构函数收回内存,但是此时该处的内存已经被收回了,这导致程序无法正常结束。

2、使用拷贝构造函数实现深层拷贝

代码示例:

#include <iostream>
#include <string.h>
using namespace std;

class MyString
{
private:
	char* buffer;

public:
	MyString(const char* initString)  // constructor
	{
		buffer = NULL;
		cout << "Default constructor: creating new MyString" << endl;
		if (initString != NULL)
		{
			buffer = new char[strlen(initString) + 1];
			strcpy_s(buffer, strlen(initString) + 1, initString);

			cout << "buffer points to: 0x" << hex;
			cout << (unsigned int*)buffer << endl;
		}
	}

	MyString(const MyString& copySource)  // Copy constructor
	{
		buffer = NULL;
		cout << "COpy constructor: copying from MyString" << endl;
		if (copySource.buffer != NULL)
		{
			// allocate own buffer
			buffer = new char[strlen(copySource.buffer) + 1];

			// deep copy from the source into local buffer
			strcpy_s(buffer, strlen(copySource.buffer) + 1, copySource.buffer);

			cout << "buffer points to: 0x" << hex;
			cout << (unsigned int*)buffer << endl;
		}
	}

	~MyString()  // Destructor
	{
		cout << "Invoking destructor, clearing up" << endl;
		delete[] buffer;
	}

	int GetLength()
	{
		return strlen(buffer);
	}
	const char* GetString()
	{
		return buffer;
	}
};

void UseMyString(MyString str) {
	cout << "String buffer in MyString is " << str.GetLength();
	cout << " charactors long" << endl;

	cout << "buffer containsL: " << str.GetString() << endl;
	return;
}

int main()
{
	MyString sayHello("Hello from String Class");
	UseMyString(sayHello);

	return 0;
}

输出:
Default constructor: creating new MyString
buffer points to: 0x01183090
COpy constructor: copying from MyString
buffer points to: 0x01182EE0
String buffer in MyString is 17 charactors long
buffer containsL: Hello from String Class
Invoking destructor, clearing up
Invoking destructor, clearing up

可见,使用拷贝构造函数的深层拷贝避免了浅层拷贝的错误。本小节的代码和上一小节的代码基本相同,但是添加了一个拷贝构造函数,当将对象sayHello传给函数UseMyString时,拷贝构造函数被激活,此时实例化sayHello创建的指针和UseMyString中str的指针指向的是两个不同的地址,因此不会导致程序出错。

需要注意的是,在拷贝构造函数的声明中使用const可确拷贝制构造函数不会修改所引用的源对象。
此外,拷贝构造函数通过引用传递参数,如果不使用引用,则拷贝构造函数本身会调用一个副本,从而再次调用自身,依此类推,直到系统内存不足为止。

注意:
当类中包含原始指针成员(char *等)时,务必添加拷贝构造函数。
使用const引用源参数作为拷贝构造函数的参数。

另外,Move Constructors有助于提高性能

六、this指针

在C++中,this是一个保留关键字,其包含对象的地址,换句话说,this关键字的值是&object。在类的方法中,调用另一个方法时,编译器将this指针作为函数调用中的隐式不可见参数发送。

class Human 
{ 
private:
	void Talk (string Statement) 
	{ 
		cout << Statement; 
	} 
public:
	void IntroduceSelf() 
	{ 
		Talk("Bla bla"); // same as Talk(this, "Bla Bla") 
	} 
};

另外this关键字还有一种用法:

void SetAge(int humansAge) 
{ 
	this->age = humansAge;    // same as age = humansAge
}

七、对类和类对象使用sizeof()

此运算符对类也有效,作用是计算类中包含的每个属性所消耗的字节总和。

八、关键字struct和class的不同之处

struct是C的关键字,并且出于所有实际目的,它对于C ++编译器来说类似于class。

九、friend关键字

类不允许外部访问其声明为private类型的成员,但可以使用关键字friend来避免此操作,代码示例如下:

#include <iostream>
#include <string>
using namespace std;

class Human
{
private:
	friend void DisplayAge(const Human& person);
	string name;
	int age;

public:
	Human(string humansName, int humansAge)
	{
		name = humansName;
		age = humansAge;
	}
};

void DisplayAge(const Human& person)
{
	cout << person.age << endl;
}

int main()
{
	Human firstMan("Adam", 25);
	cout << "Accessing private member age via friend function: ";
	DisplayAge(firstMan);

	return 0;
}

输出:
Accessing private member age via friend function: 25

十、union:一种数据存储机制

union是一种特殊的类,其中一次只有一个非静态数据成员处于活动状态。因此,union可以容纳多个数据成员,但实际上只能使用其中一个。

1、union的声明
union UnionName 
{
	Type1 member1;
	Type2 member2; 
	…
	TypeN memberN; 
};

UnionName unionObject;
unionObject.member2 = value; // choose member2 as the active member

总结

本章介绍了类的封装,了解了public和private等访问权限,介绍了构造函数、析构函数以及拷贝构造函数的概念等。

此文供学习交流使用,若有错误之处欢迎共同交流。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值