本文为 C++ 学习笔记,参考《Sams Teach Yourself C++ in One Hour a Day》第 8 版、《C++ Primer》第 5 版、《代码大全》第 2 版。
面向对象编程有四个重要的基础概念:抽象、封装、继承和多态。本文整理 C++ 中类与对象的基础内容,涉及抽象和封装两个概念。《C++基础-继承》一文讲述继承概念。《C++基础-多态》一文讲述多态概念。这些内容是 C++ 中最核心的内容。
抽象
抽象是一种忽略个性细节、提取共性特征的过程。当用“房子”指代由玻璃、混凝土、木材组成的建筑物时就是在使用抽象。当把鸟、鱼、老虎等称作“动物”时,也是在使用抽象。
基类是一种抽象,可以让用户关注派生类的共同特性而忽略各派生类的细节。类也是一种抽象,用户可以关注类的接口本身而忽视类的内部工作方式。函数接口、子系统接口都是抽象,各自位于不同的抽象层次,不同的抽象层次关注不同的内容。
抽象能使人以一种简化的观点来考虑复杂的概念,忽略繁琐的细节能大大降低思维及实现的复杂度。如果我们在看电视前要去关注塑料分子、琉璃分子、金属原子是如何组成一部电视机的、电与磁的原理是什么、图像是如何产生的,那这个电视不用看了。我们只是要用一台电视,而不关心它是怎么实现的。同理,软件设计中,如果不使用各种抽象层次,那么这一堆代码将变得无法理解无法维护甚至根本无法设计出来。
封装
抽象是从一种高层的视角来看待一个对象。而封装则是,除了那个抽象的简化视图外,不能让你看到任何其他细节。简言之,封装就是隐藏实现细节,只让你看到想给你看的。
在程序设计中,就是把类的成员(属性和行为)进行整合和分类,确定哪些成员是私有的,哪些成员是公共的,私有成员隐藏,公共成员开放。类的用户(调用者)只能访问类的公共接口。
// 类:人类
class Human
{
pubilc:
// 成员方法:
void Talk(string textToTalk); // 说话
void IntroduceSelf(); // 自我介绍
private:
// 成员属性:
string name; // 姓名
string dateOfBirth; // 生日
string placeOfBirth; // 出生地
string gender; // 性别
...
};
// 对象:具体的某个人
Human xiaoMing;
Human xiaoFang;
对象是类的实例。语句 Human xiaoMing;
和 int a;
本质上并无不同,对象和类的关系,等同于变量和类型的关系。
不介意外部知道信息使用 public 关键字限定,需要保密的信息使用 private 关键字限定。
构造函数在创建对象时被调用。执行初始化操作。
构造函数形式如下:
class Human
{
public:
Human(); // 构造函数声明
};
Human::Human() // 构造函数实现(定义)
{
...
}
可不提供实参调用的构造函数是默认构造函数,包括如下两种: 1) 不带任何函数形参的构造函数是默认构造函数 2) 带有形参但所有形参都提供默认值的构造函数也是默认构造函数,因为这种既可以携带实参调用,也可以不带实参调用
当用户未给出任何构造函数时,编译器会自动生成一个构造函数,叫作合成的默认构造函数,此函数对类的数据成员初始化规则如下: 1) 若数据成员存在类内初始化值,则用这个初始化值来初始化数据成员 2) 否则,执行默认初始化。默认值由数据类型确定。参"C++ Primer 5th"第 40 页
下面这个类因为没有任何构造函数,所以编译器会生成合成的默认构造函数:
class Human
{
pubilc:
// 成员方法:
void Talk(string textToTalk); // 说话
void IntroduceSelf(); // 自我介绍
private:
// 成员属性:
string name; // 姓名
string dateOfBirth; // 生日
string placeOfBirth; // 出生地
string gender; // 性别
};
函数可以有带默认值的参数,构造函数当然也可以。
class Human
{
private:
string name;
int age;
public:
// overloaded constructor (no default constructor)
Human(string humansName, int humansAge = 25)
{
name = humansName;
age = humansAge;
...
};
可以使用如下形式的实例化
Human adam("Adam"); // adam.age is assigned a default value 25
Human eve("Eve, 18); // eve.age is assigned 18 as specified
初始化列表是一种简写形式,将相关数据成员的初始化列表写在函数名括号后,从而可以省略函数体中的相应数据成员赋值语句。
Human::Human(string humansName, int humansAge) : name(humansName), age(humansAge)
{
}
上面这种写法和下面这种写法具有同样的效果:
Human::Human(string humansName, int humansAge)
{
name = humansName;
age = humansAge;
}
复制一个类的对象时,只复制其指针成员但不复制指针指向的缓冲区,其结果是两个对象指向同一块动态分配的内存。销毁其中一个对象时,delete[] 释放这个内存块,导致另一个对象存储的指针拷贝无效。这种复制被称为浅复制。
如下为浅复制的一个示例程序:
#include <iostream>
#include <string.h>
using namespace std;
class MyString
{
private:
char* buffer;
public:
MyString(const char* initString)
{
buffer = NULL;
if(initString != NULL)
{
buffer = new char [strlen(initString) + 1];
strcpy(buffer, initString);
}
}
~MyString()
{
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 << " characters long" << endl;
cout << "buffer contains: " << str.GetString() << endl;
return;
}
int main()
{
MyString sayHello("Hello from String Class");
UseMyString(sayHello);
return 0;
}
分析一下 UseMyString(sayHello);
这一语句:
复制构造函数是一个重载的构造函数,由编写类的程序员提供。每当对象被复制时,编译器都将调用复制构造函数。
复制构造函数函数语法如下:
class MyString
{
MyString(const MyString& copySource); // copy constructor
};
MyString::MyString(const MyString& copySource)
{
// Copy constructor implementation code
}
复制构造函数接受一个以引用方式传入的当前类的对象作为参数。这个参数是源对象的别名,您使用它来编写自定义的复制代码,确保对所有缓冲区进行深复制。
复制构造函数的参数必须按引用传递,否则复制构造函数将不断调用自己,直到耗尽系统的内存为止。原因就是每当对象被复制时,编译器都将调用复制构造函数,如果参数不是引用,实参不断复制给形参,将生成不断复制不断调用复制构造函数。
示例程序如下:
#include <iostream>
#include <string.h>
using namespace std;
class MyString
{
private:
char* buffer;
public:
MyString() {}
MyString(const char* initString) // constructor
{
buffer = NULL;
cout << "Default constructor: creating new MyString" << endl;
if(initString != NULL)
{
buffer = new char [strlen(initString) + 1];
strcpy(buffer, 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(buffer, copySource.buffer);
cout << "buffer points to: 0x" << hex;
cout << (unsigned int*)buffer << endl;
}
}
// Destructor
~MyString()
{
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 << " characters long" << endl;
cout << "buffer contains: " << str.GetString() << endl;
return;
}
int main()
{
MyString sayHello("Hello from String Class");
UseMyString(sayHello);
return 0;
}
再看 UseMyString(sayHello);
这一语句:
同样,如果没有提供复制赋值运算符 operator=,编译器提供的默认复制赋值运算符将导致浅复制。
关于复制构造函数的注意事项如下:
class MyString
{
// 代码同上一示例程序,此处略
};
MyString Copy(MyString& source)
{
MyString copyForReturn(source.GetString()); // create copy
return copyForReturn; // 1. 将返回值复制给调用者,首次调用复制构造函数
}
int main()
{
MyString sayHello ("Hello World of C++");
MyString sayHelloAgain(Copy(sayHello)); // 2. 将 Copy() 返回值作实参,再次调用复制构造函数
return 0;
}
上例中,参考注释,实例化 sayHelloAgain 对象时,复制构造函数被调用了两次。如果对象很大,两次复制造成的性能影响不容忽视。
为避免这种性能瓶颈, C++11 引入了移动构造函数。移动构造函数的语法如下:
// move constructor
MyString(MyString&& moveSource)
{
if(moveSource.buffer != NULL)
{
buffer = moveSource.buffer; // take ownership i.e. 'move'
moveSource.buffer = NULL; // set the move source to NULL
}
}
有移动构造函数时,编译器将自动使用它来“移动”临时资源,从而避免深复制。增加移动构造函数后,上一示例中,将首先调用移动构造函数,然后调用复制构造函数,复制构造函数只被会调用一次。
析构函数在对象销毁时被调用。执行去初始化操作。
每当对象不再在作用域内或通过 delete 被删除进而被销毁时,都将调用析构函数。这使得析构函数成为重置变量以及释放动态分配的内存和其他资源的理想场所。
假设要模拟国家政体,一个国家只能一位总统,President 类的对象不允许复制。
要禁止类对象被复制,可将复制构造函数声明为私有的。为禁止赋值,可将赋值运算符声明为私有的。复制构造函数和赋值运算符声明为私有的即可,不需要实现。这样,如果代码中有对对象的复制或赋值,将无法编译通过。形式如下:
class President
{
private:
President(const President&); // private copy constructor
President& operator= (const President&); // private copy assignment operator
// … other attributes
};
前面讨论的 President 不能复制,不能赋值,但存在一个缺陷:无法禁止通过实例化多个对象来创建多名总统:
President One, Two, Three;
要确保一个类不能有多个实例,也就是单例的概念。实现单例,要使用私有构造函数、私有赋值运算符和静态实例成员。
将关键字 static 用于类的数据成员时,该数据成员将在所有实例之间共享。 将关键字 static 用于成员函数(方法)时,该方法将在所有成员之间共享。 将 static 用于函数中声明的局部变量时,该变量的值将在两次调用之间保持不变。
将析构函数声明为私有的。略
略
在类中,关键字 this 包含当前对象的地址,换句话说, 其值为&object。在类成员方法中调用其他成员方法时, 编译器将隐式地传递 this 指针。
调用静态方法时,不会隐式地传递 this 指针,因为静态函数不与类实例相关联,而由所有实例共享。要在静态函数中使用实例变量,应显式地声明一个形参,并将实参设置为 this 指针。
sizeof 用于类时,值为类声明中所有数据属性占用的总内存量,单位为字节。是否考虑对齐,与编译器有关。
结构 struct 与类 class 非常相似,差别在于程序员未指定时,默认的访问限定符(public 和 private)不同。因此,除非指定了,否则结构中的成员默认为公有的(而类成员默认为私有的);另外,除非指定了,否则结构以公有方式继承基结构(而类为私有继承)。