C++学习笔记(一)

187 阅读23分钟

前言

通过书和视频来学习 C++ 都是很不错的学习方式,这里贴一下视频学习链接:

第一个 C++ 程序

// Preprocessor directive that includes header iostream 
#include <iostream> 

 // Start of your program: function block main() 
 int main() 
 { 
     /* Write to the screen */ 
    std::cout << "Hello World" << std::endl; 
    
    std::cin.get();
 
    // Return a value to the OS 
    return 0; 
}

这个 C++ 程序用于在屏幕上输出 Hello World。

#include <iostream> 是一条预处理指令,在 C++ 中,# 符号后的都是预处理指令,编译器会在实际编译之前先处理预处理指令。 #include <filename> 让预处理器获取指定文件(这里是 iostream)的内容,并将它拷贝到现在的文件内,使用 #include 包含的文件称为头文件。iostream 是一个标准头文件,没有这行代码,就无法使用 std::cout 和 std::cin。

其中 cout 是命名空间 std 中定义的一个流,<< 看起来很奇怪,它叫运算符重载,后面我们会详细讲解它,你可以把它理解成一个 printf(const char *) 函数,用于把 "Hello World" 打印出来,std::endl 用于换行。

cin.get() 的作用是让用户输入字符,在这里用于暂停程序的执行,否则控制台窗口打开后马上就消失了。

这里可以看到 main() 函数前面有一个 int,表示 main() 函数的返回值是 int 类型的,但是由于 main() 函数比较特殊,最后一行的 return 0 可以不写,main() 函数默认返回 0。

代码组织方式

在 C++ 中,通常的代码组织方式是:

  1. 头文件(.h/.hpp) 包含以下内容:
  • 函数声明(也称为函数原型);
  • 类定义;
  • 宏定义;
  • 类型定义;
  1. 源文件(.cpp) 包含以下内容:
  • 函数的具体实现(定义);
  • 变量的定义;

也就是说

  1. 在头文件中进行 声明 (declaration)
  2. 在源文件中进行 定义 (definition)

命名空间

在上面的代码中 std::cout 表示使用位于标准(std)命名空间中的 cout,你也可以在代码前面使用声明 using namespace std,告诉编译器您要使用命名空间 std,后面在使用 cout 和 endl 时,就无需显式地指定命名空间了。代码如下:

// Preprocessor directive 
#include <iostream>

// Start of your program 
int main()
{
	// Tell the compiler what namespace to search in 
	using namespace std;
	
	/* Write to the screen using std::cout */
	cout << "Hello World" << endl;
	
    // Return a value to the OS 
	return 0;
}

这里的代码没有使用 cin.get(),控制台窗口默认会在程序执行完毕后立即关闭。使用调试模式运行(Ctrl+F5),可以让程序执行完后不立即退出。

sizeof

在 C++ 中可以使用 sizeof 获取变量的长度,单位为字节。

auto

使用 auto 关键字可以不指定变量的类型:

auto coinFlippedHeads = true;

编译器会通过变量的初始值自动确定变量的类型。

typeof

可以使用 typedef 关键字将变量类型替换成另外的名称:

typedef unsigned int STRICTLY_POSITIVE_INTEGER;
STRICTLY_POSITIVE_INTEGER numEggsInBasket = 4532;

如上,给 unsigned int 指定了一个更具描述性的名称。

constexpr

关键字 constexpr 可用于声明常量表达式:

constexpr double GetPi() {return 22.0 / 7;}

在一个常量表达式中,可使用另一个常量表达式:

constexpr double TwicePi() {return 2 * GetPi();}

这样代码在编译阶段会进行优化,编译器会将所有的 TwicePi() 都替换为 6.28571,从而避免在代码执行时计算 2×22/7 的值。

枚举

在 C++ 中可以使用关键字 enum 来声明枚举:

enum Direction
{
    North,
    South,
    East,
    West
};

这样声明的变量只能取指定的值:

Direction walkDirection = North; // Initial value

编译器会将枚举值转换为整数,每个枚举值都比前一个大 1。您可以指定起始值,如果没有指定,编译器认为起始值为 0,因此这里 North 的值为 0。如果愿意,还可通过初始化显式地给每个枚举量指定值。

使用 #define 定义常量

#define 是一个预处理器宏,如下代码让预处理器将随后出现的所有 pi 都替换为 3.14286:

#define pi 3.14286

使用 #define 定义常量的做法已被摒弃,因此不应采用这种做法。

动态数组

动态数组可用于在代码执行阶段需要动态增加数组容量的场景:

#include <iostream> 
#include <vector>

using namespace std;

int main()
{
	vector<int> dynArray(3); // dynamic array of int 
	
	dynArray[0] = 365;
	dynArray[1] = -421;
	dynArray[2] = 789;
	
	cout << "Number of integers in array: " << dynArray.size() << endl;
	
	cout << "Enter another element to insert" << endl;
	int newValue = 0;
	cin >> newValue;
	dynArray.push_back(newValue);
	
	cout << "Number of integers in array: " << dynArray.size() << endl;
	cout << "Last element in array: ";
	cout << dynArray[dynArray.size() - 1] << endl;
	int m;
	cin >> m;
	return 0;	
}

打印如下:

Number of integers in array: 3
Enter another element to insert
18
Number of integers in array: 4
Last element in array: 18

vector 称为矢量,代码中使用 push_back() 将用户输入的数字插入到数组末尾,数组的容量从 3 变成 4。

指针和引用

  1. 什么是指针

指针是一种指向内存地址的特殊变量。

  1. 如何申明指针

申明指针前面需要带上类型,比如 int,表示指针指向的内存地址存储了一个整数,如下所示:

int *pointsToInt = NULL;

跟大多数变量一样,声明指针的时候需要对它赋一个初始值,否则它的值就是随机的内存地址。这里把指针初始化为 NULL,NULL 不是一个内存地址,且是一个可以检查的值。

  1. 使用引用运算符(&)获取变量地址

在变量前面加上 & 可以获取变量的内存地址,看下面的代码:

#include <stdio.h>

#include <iostream> 
using namespace std;

int main()
{
	int age = 30;
	const double Pi = 3.1416;
	
	// Use & to find the address in memory 
	cout << "Integer age is located at: 0x" << &age << endl;
	cout << "Double Pi is located at: 0x" << &Pi << endl;
	
	return 0;
}

运行后打印如下:

Integer age is located at: 0x0093FB48
Double Pi is located at: 0x0093FB38

4. 使用指针存储地址

比如,你申明了一个 int 类型的变量 age:

int age = 30;

接下来可以声明一个 int 指针来指向 age 的地址:

int* pointsToInt = &age; 

5. 使用解除引用运算符(*)访问指针指向的数据

比如有一个指针 pData,要访问该指针指向的内存地址所存储的数据,可以使用 *pData。

  1. 动态内存分配

如果使用下面的方式声明数组:

int myNums[100]; // a static array of 100 integers

程序将存在两个问题:

  1. 限制了数组的容量,该数组无法存储 100 个以上的数字。
  2. 如果只需要存储 1 个数字,却为 100 个数字预留存储空间,这将降低系统的性能。

导致这些问题的原因是,这里数组的内存分配是静态和固定的。想要动态地分配和释放内存,需要使用 new 和 delete 运算符,它让你能够根据需要分配和释放内存。

可以使用 new 来分配新的内存块,如果成功,将返回一个指针,该指针指向分配的内存。使用 new 时,需要指定要为哪种数据类型分配内存。代码如下所示:

int* pointToAnInt = new int; // get a pointer to an integer
int* pointToNums = new int[10]; // pointer to a block of 10 integers

new 表示请求分配内存,并不能保证分配请求总能得到满足,因为这取决于系统的状态以及内存资源的可用性。

使用 new 分配的内存最终都需使用对应的 delete 进行释放:

Type* Pointer = new Type; // allocate memory
delete Pointer; // release memory allocated above

这种规则也适用于为多个元素分配的内存:

Type* Pointer = new Type[numElements]; // allocate a block
delete[] Pointer; // release block allocated above

7. 将关键字 const 用于指针

指针也是变量,因此也可将关键字 const 用于指针。然而,指针是特殊的变量,指向内存地址,还可用于修改指针指向的数据。因此,const 指针有如下三种:

  • 不能修改指针包含的地址,但可修改指针指向的数据:
int daysInMonth = 30;
int* const pDaysInMonth = &daysInMonth;
*pDaysInMonth = 31; // OK!指针指向的数据可以修改
//int daysInLunarMonth = 28;
//pDaysInMonth = &daysInLunarMonth; // Not OK! 地址不能修改
  • 不能修改指针指向的数据,但可以修改指针包含的地址,即指针可以指向其他地方:
int hoursInDay = 24;
const int* pointsToInt = &hoursInDay;
int monthsInYear = 12;
pointsToInt = &monthsInYear; // OK! 地址可以修改
//*pointsToInt = 13; // Not OK! 指针指向的数据不可以修改
//int* newPointer = pointsToInt; // Not OK! 不能把 const 类型赋值给 non-const 类型

• 指针包含的地址以及指向的值都不能修改(这种组合最严格):

int hoursInDay = 24;
const int* const pHoursInDay = &hoursInDay;
//*pHoursInDay = 25; // Not OK! 指针指向的数据不可以修改
//int daysInMonth = 30;
//pHoursInDay = &daysInMonth; // Not OK! 地址不可以修改

将指针作为参数传递给函数时,这些形式的 const 很有用。这里函数参数应声明为最严格的 const 指针,这样可以禁止程序员随意修改指针及其指向的数据。

  1. 引用

引用是变量的别名。要声明引用,可使用引用运算符(&),如下面的语句所示:

VarType original = Value;
VarType& ReferenceVariable = original;

声明和使用引用的方法如下:

#include <iostream>
using namespace std;

int main()
{
    int original = 30;
    cout << "original = " << original << endl;
    cout << "original is at address: " << hex << &original << endl;

    int& ref1 = original;
    cout << "ref1 is at address: " << hex << &ref1 << endl;

    int& ref2 = ref1;

    cout << "ref2 is at address: " << hex << &ref2 << endl;
    cout << "Therefore, ref2 = " << dec << ref2 << endl;
    return 0;
}

运行后打印如下:

    original = 30
    original is at address: 0099F764
    ref1 is at address: 0099F764
    ref2 is at address: 0099F764
    Therefore, ref2 = 30

可以看到,无论将引用初始化为变量(第 10 行)还是其他引用(第 13 行),它都指向相应变量所在的内存单元。因此,引用是真正的别名,即相应变量的另一个名字。

  1. 引用的作用

引用让您能够访问相应变量所在的内存单元,写函数时引用很有用。下面是一个典型的函数声明:

ReturnType DoSomething(Type parameter);

调用函数 DoSomething() 的代码类似于下面这样:

ReturnType Result = DoSomething(argument); // function call

上述代码会将 argument 复制给 parameter,再被函数 DoSomething() 使用。如果 argument 占用了大量内存,这个复制步骤的开销将很大。同时,DoSomething() 有返回值,这个值被复制给 Result。如果能避免这些复制步骤,让函数直接使用调用者栈中的数据就太好了。

使用引用可以避免这些复制步骤,如下所示,修改函数接收一个引用类型的参数:

ReturnType DoSomething(Type& parameter); // note the reference&

调用该函数的代码类似下面这样:

ReturnType Result = DoSomething(argument);

由于 argument 是按引用传递的,parameter 不再是 argument 的拷贝,而是它的别名,另外,执行完 DoSomething(argument) 后还可以继续使用 argument ,代码如下:

#include <iostream>
using namespace std;

void GetSquare(int& number)
{
    number *= number;
}

int main()
{
    cout << "Enter a number you wish to square: ";
    int number = 0;
    cin >> number;

    GetSquare(number);
    cout << "Square is: " << number << endl;
    return 0;
}

输出如下:

Enter a number you wish to square: 5
Square is: 25

如果忘记将参数 number 声明为引用(&),在 main() 函数中将无法拿到 GetSquare() 函数的计算结果,因为 GetSquare() 将使用 number 的本地拷贝执行运算,而函数结束时该拷贝将被销毁。通过使用引用,可确保 GetSquare() 对 main() 中定义的 number 所在的内存单元进行操作。这样,函数 GetSquare() 执行完毕后,还可以在 main() 中使用运算结果。

  1. 将关键字 const 用于引用

可以使用关键字 const 禁止通过引用修改它指向的变量的值:

int original = 30;
const int& constRef = original;
//constRef = 40; // Not allowed: constRef can’t change value in original
//int& ref2 = constRef; // Not allowed: ref2 is not const
const int& constRef2 = constRef; // OK

前面的示例中直接修改了 number 的值来计算 number 的平方,这个代码不太严谨,正常情况下应该不能在 GetSquare() 函数中修改 number 的值。可以将 number 的声明前面加上 const 关键字,并将结果存到 result 中,代码如下:

#include <iostream>
using namespace std;

void GetSquare(const int& number, int& result)
{
    result = number*number;
}

int main()
{
    cout << "Enter a number you wish to square: ";
    int number = 0;
    cin >> number;

    int square = 0;
    GetSquare(number, square);
    cout << number << "^2 = " << square << endl;

    return 0;
}

打印如下:

Enter a number you wish to square: 27
27^2 = 729

这里使用了两个参数,一个用于接受输入,另一个用于存储运算结果。

类和对象

下面是一个模拟人类的类:

class Human{
    // Member attributes:
    string name;
    string dateOfBirth;
    string placeOfBirth;
    string gender;

    // Member functions:
    void Talk(string textToTalk);
    void IntroduceSelf();
    ...
};

创建 Human 对象与创建其他基础数据类型(如 double)类似:

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

就像可以为其他类型(如 int)动态分配内存一样,也可使用 new 为 Human 对象动态地分配内存:

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

使用句点运算符访问成员

firstMan 有 dateOfBirth 等属性,可使用句点运算符(.)来访问:

firstMan.dateOfBirth = "1970";

这也适用于 IntroduceSelf( ) 等方法:

firstMan.IntroduceSelf();

如果有一个指针 firstWoman,它指向 Human 类的一个实例,则可使用指针运算符(->)来访问成员,也可使用间接运算符(*)来获取对象,再使用句点运算符来访问成员:

Human* firstWoman = new Human();
(*firstWoman).IntroduceSelf();

使用指针运算符(->)访问成员

如果对象是使用 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 << " years 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 = new Human();
	firstWoman->name = "Eve";
	firstWoman->age = 28;

	firstMan.IntroduceSelf();
	firstWoman->IntroduceSelf();
	delete firstWoman;
}

输出:

I am Adam and am 30 years old
I am Eve and am 28 years old

关键字 public 和 private

C++让您能够将类属性和方法声明为公有的,这意味着有了对象后就可访问它们;也可将其声明为私有的,这意味着只能在类的内部(或其友元)中访问。

构造函数

构造函数可以在类声明中实现,也可以在类声明外实现,在类声明中实现的构造函数代码如下:

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

在类声明外定义构造函数的代码如下:

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

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

:: 被称为作用域解析运算符。例如,Human::dateOfBirth 指的是在 Human 类中声明的变量 dateOfBirth,而 ::dateOfBirth 表示全局作用域中的变量 dateOfBirth。

如果你没有写默认构造函数(不带参数的构造函数),但是写了重载的构造函数(带参数的构造函数)时,C++ 编译器不会生成默认构造函数,这时候创建实例的时候只能调用重载的构造函数。

默认构造函数是调用时可不提供参数的构造函数,而并不一定是不接受任何参数的构造函数。比如,下面的构造函数虽然有两个参数,但它们都有默认值,因此也是默认构造函数:

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 ";
        cout << name << " of age " << age;
    }
};

析构函数

与构造函数一样,析构函数也是一种特殊的函数。构造函数在实例化对象时被调用,而析构函数在对象销毁时自动被调用。

析构函数看起来像一个与类同名的函数,但前面有一个腭化符号(~)。因此,Human 类的析构函数的声明类似于下面这样:

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

析构函数可在类声明中实现,也可在类声明外实现。在类声明中实现(定义)析构函数的代码类似于下面这样:

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

在类声明外定义析构函数的代码类似于下面这样:

class Human{
public:
    ~Human(); // destructor declaration
};

// destructor definition (implementation)
Human::~Human(){
    // destructor code here
}

正如您看到的,析构函数的声明与构造函数稍有不同,那就是包含腭化符号(~)。然而,析构函数的作用与构造函数完全相反。

每当对象不再在作用域内或通过 delete 被删除进而被销毁时,都将调用析构函数。这使得析构函数成为重置变量以及释放动态分配的内存和其他资源的理想场所。

使用 char* 缓冲区时,你必须自己管理内存的分配和释放,因此建议不要使用它,建议使用 std::string。std::string 充分利用了构造函数和析构函数,让您无需考虑分配和释放等内存管理工作。

如下所示是一个名为 MyString 的类,在构造函数中为一个字符串分配内存,并在析构函数中释放它。

#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(buffer, 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

这样你在使用 MyString::buffer 时就不需要考虑内存的分配和释放了。

拷贝构造函数

有一个函数 Area():

double Area(double radius)
{
    return Pi * radius * radius;
}

int main()
 {
    cout << "Enter radius: ";
    double radius = 0;
    cin >> radius;
    
    // Call function "Area"
    cout << "Area is: " << Area(radius) << endl;
}

当在 main() 函数中调用 Area() 时,实参被复制给形参 radius,这种规则也适用于对象(类的实例)

浅拷贝及其存在的问题

看下面的代码:

#define _CRT_SECURE_NO_WARNINGS
#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(buffer, initString);
         }
    }

    ~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 << " characters 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 characters long
buffer contains: Hello from String Class
Invoking destructor, clearing up
Invoking destructor, clearing up

然后报错了,报错信息如下:

image.png

这里在 main() 中调用了函数 UseMyString(),并传入 sayHello 对象,sayHello 被复制给形参 str,因此 sayHello.buffer 和 str.buffer 指向同一个内存单元,函数 UseMyString() 返回时,变量 str 被销毁,此时将调用 MyString 类的析构函数,使用 delete[] 释放分配给 buffer 的内存,同时将导致 sayHello.buffer 指向的内存单元无效,等 main() 执行完毕时,对不再有效的内存地址调用 delete,正是这种重复调用 delete 导致了程序崩溃。

使用拷贝构造函数确保深拷贝

拷贝构造函数是一个重载的构造函数,由编写类的程序员提供。每当对象被拷贝时,编译器都将调用拷贝构造函数。拷贝构造函数接收以引用的方式传入的当前类的对象作为参数,这个参数是源对象的别名,您使用它来编写自定义的拷贝代码,确保对所有缓冲区进行深拷贝,其语法如下:

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(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;
		}
	}

	// 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;
}

运行后打印如下:

Default constructor: creating new MyString
buffer points to: 0x01617898
Copy constructor: copying from MyString
buffer points to: 0x0161E710
String buffer in MyString is 17 characters long
buffer contains: Hello from String Class
Invoking destructor, clearing up
Invoking destructor, clearing up

可以看到代码跟之前差不多,只是新增了一个拷贝构造函数,拷贝构造函数除了参数跟构造函数不一样,其他代码一模一样。函数 UseMyString() 的参数为 MyString str,调用该函数时将创建新的 MyString 的实例 str。在第 72 行,sayHello 按值传递给函数 UseMyString(),这将自动调用拷贝构造函数。在拷贝构造函数中,分配了新的内存给 buffer,从输出也可以看出来,sayHello.buffer 与 str.buffer 指向的内存地址不同,函数 UseMyString() 返回、形参 str 被销毁时,析构函数对拷贝构造函数分配的内存地址调用 delete[],不会影响 main() 中 sayHello 指向的内存。因此,这两个函数都执行完毕时,成功地销毁了各自的对象,没有导致应用程序崩溃。

不允许拷贝的类

假设您需要模拟国家的政体。一个国家只能有一位总统,而 President 类面临如下风险:

President ourPresident;
DoSomething(ourPresident); // duplicate created in passing by value
President clone;
clone = ourPresident; // duplicate via assignment

如果您不声明拷贝构造函数,C++ 将为您添加一个公有的默认拷贝构造函数,要禁止类对象被拷贝,可声明一个私有的拷贝构造函数。这确保函数调用 DoSomething(OurPresident) 无法通过编译。为禁止赋值,可声明一个私有的赋值运算符。这样修改后代码如下:

class President
{
private:
    President(const President&); // private copy constructor
    President& operator= (const President&); // private copy assignment operator

    // … other attributes
};

无需给私有拷贝构造函数和私有赋值运算符提供实现,只需将它们声明为私有的就足以实现目标:确保 President 的对象是不可复制的。

单例类

前面讨论的 President 类很不错,但还需要有个方法,通过这个方法只能创建一个 President 对象。要禁止创建其他的 President 对象,可使用单例的概念,要创建单例类,就要使用 static 关键字,如下是一个单例类 President,它禁止拷贝、赋值以及创建多个实例:

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

class President
{
private:
	President() {}; // private default constructor 
	President(const President&); // private copy constructor 
	const President& operator=(const President&); // assignment operator 
	
	string name;
	
public:
	static President& GetInstance()
	{
		// static objects are constructed only once 
		static President onlyInstance;
		return onlyInstance;
	}

	string GetName()
	{ return name; }
			
	void SetName(string InputName)
	{ name = InputName; }
};

int main()
{
	President& onlyPresident = President::GetInstance();
	onlyPresident.SetName("Abraham Lincoln");
	
	// uncomment lines to see how compile failures prohibit duplicates 
	// President second; // cannot access constructor 
	// President* third= new President(); // cannot access constructor 
	// President fourth = onlyPresident; // cannot access copy constructor 
	// onlyPresident = President::GetInstance(); // cannot access operator= 
	
	cout << "The name of the President is: ";
	cout << President::GetInstance().GetName() << endl;
}

在 main() 函数中包含大量注释,演示了各种创建 President 实例和拷贝的方式,它们都无法 通过编译。

第 35 和 36 行分别试图使用默认构造函数在堆和自由存储区中创建对象,但默认构造函数不可用,因为它是私有的,如第 8 行所示。

第 37 行试图使用拷贝构造函数创建现有对象的拷贝(在创建对象的同时赋值将调用拷贝构造函 数),但在 main() 中不能使用拷贝构造函数,因为第 9 行将其声明成了私有的。

第 38 行试图通过赋值创建对象的拷贝,但行不通,因为第 10 行将赋值运算符声明成了私有的。

因此,在 main() 中,不能创建 President 类的实例,唯一的方法是使用静态函数GetInstance() 来获取 President 的实例。GetInstance() 是静态成员,类似于全局函数,无需通过对象来调用它。

禁止在栈中实例化类

栈空间是有限的,如果一个类占几 TB 的数据,你需要禁止在栈中实例化它,如何实现呢?你可以把析构函数声明成私有的:

class MonsterDB
{
private:
    ~MonsterDB(); // private destructor
    //... members that consume a huge amount of data
};

这样下面的代码将会编译报错:

int main()
{
    MonsterDB myDatabase; // compile error
    // … more code
    return 0;
}

因为退栈时将弹出栈中的所有对象,编译器需要在 main() 末尾调用析构函数~MonsterDB(),但这个析构函数是私有的,因此上述语句将编译错误。

将析构函数声明为私有的并不能禁止在堆中实例化:

int main()
{
    MonsterDB* myDatabase = new MonsterDB(); // no error
    // … more code
    return 0;
}

上述代码不会编译报错,但是会导致内存泄漏。由于在 main() 中不能调用析构函数,因此也不能调用 delete。为了解决这种问题,需要在 MonsterDB 类中提供一个销毁实例的静态公有函数(作为类成员,它能够调用析构函数),代码如下:

#include <iostream> 
using namespace std;

class MonsterDB
{
private:
	~MonsterDB() {}; // private destructor prevents instances on stack 
	
public:
	static void DestroyInstance(MonsterDB* pInstance)
	{
		delete pInstance; // member can invoke private destructor 
	 }
	
	 void DoSomething() {} // sample empty member method 
};

int main()
{
	MonsterDB* myDB = new MonsterDB(); // on heap 
	myDB->DoSomething();
	
	// uncomment next line to see compile failure 
	// delete myDB; // private destructor cannot be invoked 
	
	// use static member to release memory 
	 MonsterDB::DestroyInstance(myDB);
	
	return 0;
}

这样即可达到目的。

结构与类

C 语言中的 struct 与类极其相似,区别在于结构中的成员默认为公有的,而类成员默认为私有的,另外,除非指定了,否则结构以公有方式继承基结构,而类为私有继承。

声明友元

一般来说是不能从外部访问类的私有数据成员和方法的,但这条规则不适用于友元类和友元函数。要声明友元类或友元函数,可使用关键字 friend。

共用体

共用体是一种特殊的类,每次只有一个非静态数据成员处于活动状态。共用体与类一样,可包含多个数据成员,但不同的是只能使用其中的一个。代码让如下:

union UnionName
{
    Type1 member1;
    Type2 member2;
    …
    TypeN memberN;
};

要实例化并使用共用体,可像下面这样做:

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

与结构类似,共用体的成员默认也是公有的,但不同的是,共用体不能继承。另外,将 sizeof() 用于共用体时,结果总是为共用体最大成员的长度,即便该成员并不处于活动状态。