动态内存分配
如果使用下面的方式声明数组:
c++
int myNums[100]; // a static array of 100 integers
程序将存在两个问题:
- 限制了数组的容量,该数组无法存储 100 个以上的数字。
- 如果只需要存储 1 个数字,却为 100 个数字预留存储空间,这将降低系统的性能。
导致这些问题的原因是,这里数组的内存分配是静态和固定的。想要动态地分配和释放内存,需要使用 new 和 delete 运算符。
使用 new 和 delete 动态地分配和释放内存
可以使用 new 来分配新的内存块,如果成功,将返回一个指针,该指针指向分配的内存。使用 new 时,需要指定要为哪种数据类型分配内存。代码如下所示:
c++
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 进行释放:
dart
Type* Pointer = new Type; // allocate memory
delete Pointer; // release memory allocated above
这种规则也适用于为多个元素分配的内存:
scss
Type* Pointer = new Type[numElements]; // allocate a block
delete[] Pointer; // release block allocated above
将关键字 const 用于指针
指针也是变量,因此也可将关键字 const 用于指针。然而,指针是特殊的变量,包含内存地址,还可用于修改指针指向的数据。因此,const 指针有如下三种:
- 指针包含的地址是常量,不能修改,但可修改指针指向的数据:
c++
int daysInMonth = 30;
int* const pDaysInMonth = &daysInMonth;
*pDaysInMonth = 31; // OK!指针指向的数据可以修改
int daysInLunarMonth = 28;
pDaysInMonth = &daysInLunarMonth; // Not OK! 地址不能修改
- 指针指向的数据为常量,不能修改,但可以修改指针包含的地址,即指针可以指向其他地方:
c++
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 类型
• 指针包含的地址以及它指向的值都是常量,不能修改(这种组合最严格):
c++
int hoursInDay = 24;
const int* const pHoursInDay = &hoursInDay;
*pHoursInDay = 25; // Not OK! 指针指向的数据不可以修改
int daysInMonth = 30;
pHoursInDay = &daysInMonth; // Not OK! 地址不可以修改
将指针传递给函数时,这些形式的 const 很有用。函数参数应声明为最严格的 const 指针,以确保函数不会修改指针指向的值。这可禁止程序员修改指针及其指向的数据。
引用
引用是变量的别名。
要声明引用,可使用引用运算符(&),如下面的语句所示:
ini
VarType original = Value;
VarType& ReferenceVariable = original;
声明和使用引用的方法如下:
c++
#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;
}
运行后打印如下:
ini
original = 30
original is at address: 0099F764
ref1 is at address: 0099F764
ref2 is at address: 0099F764
Therefore, ref2 = 30
可以看到,无论将引用初始化为变量还是其他引用,它都指向相应变量所在的内存单元。因此,引用是真正的别名,即相应变量的另一个名字。
引用的作用
引用让您能够访问相应变量所在的内存单元,这使得编写函数时引用很有用。典型的函数声明类似于下面这样:
scss
ReturnType DoSomething(Type parameter);
调用函数 DoSomething() 的代码类似于下面这样:
ini
ReturnType Result = DoSomething(argument); // function call
上述代码导致将 argument 的值复制给 Parameter,再被函数 DoSomething() 使用。如果 argument 占用了大量内存,这个复制步骤的开销将很大。同样,当 DoSomething() 返回值时,这个值被复制给 Result。如果能避免这些复制步骤,让函数直接使用调用者栈中的数据就太好了。
使用引用可以避免这些复制步骤,如下所示,修改函数接收一个引用类型的参数:
scss
ReturnType DoSomething(Type& parameter); // note the reference&
调用该函数的代码类似于下面这样:
ini
ReturnType Result = DoSomething(argument);
由于 argument 是按引用传递的,parameter 不再是 argument 的拷贝,而是它的别名,另外,执行完 DoSomething(argument) 后还可以继续使用 argument ,代码如下:
c++
#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;
}
输出如下:
css
Enter a number you wish to square: 5
Square is: 25
如果忘记将参数 number 声明为引用(&),number 打印的值将是 0,因为 GetSquare() 将使用 number 的本地拷贝执行运算,而函数结束时该拷贝将被销毁。通过使用引用,可确保 GetSquare() 对 main() 中定义的 number 所在的内存单元进行操作。这样,函数 GetSquare() 执行完毕后,还可以在 main() 中使用运算结果。
将关键字 const 用于引用
可以使用关键字 const 禁止通过引用修改它指向的变量的值
c++
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 中,代码如下:
c++
#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;
}
打印如下:
css
Enter a number you wish to square: 27
27^2 = 729
这里使用了两个参数,一个用于接受输入,另一个用于存储运算结果。
类和对象
下面是一个模拟人类的类:
c++
class Human{
// Member attributes:
string name;
string dateOfBirth;
string placeOfBirth;
string gender;
// Member functions:
void Talk(string textToTalk);
void IntroduceSelf();
...
};
创建 Human 对象与创建其他基础数据类型(如 double)的实例类似:
c++
double pi= 3.1415; // a variable of type double
Human firstMan; // firstMan: an object of class Human
就像可以为其他类型(如 int)动态分配内存一样,也可使用 new 为 Human 对象动态地分配内存:
c++
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 等属性,可使用句点运算符(.)来访问:
ini
firstMan.dateOfBirth = "1970";
这也适用于 IntroduceSelf( ) 等方法:
ini
firstMan.IntroduceSelf();
如果有一个指针 firstWoman,它指向 Human 类的一个实例,则可使用指针运算符(->)来访问成员,也可使用间接运算符(*)来获取对象,再使用句点运算符来访问成员:
ini
Human* firstWoman = new Human();
(*firstWoman).IntroduceSelf();
使用指针运算符(->)访问成员
如果对象是使用 new 在自由存储区中实例化的,或者有指向对象的指针,则可使用指针运算符(->)来访问成员属性和方法:
ini
Human* firstWoman = new Human();
firstWoman->dateOfBirth = "1970";
firstWoman->IntroduceSelf();
delete firstWoman;
完整代码如下:
c++
#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;
firstWoman.name = "Eve";
firstWoman.age = 28;
firstMan.IntroduceSelf();
firstWoman.IntroduceSelf();
}
输出:
css
I am Adam and am 30 years old
I am Eve and am 28 years old
关键字 public 和 private
C++让您能够将类属性和方法声明为公有的,这意味着有了对象后就可获取它们;也可将其声明为私有的,这意味着只能在类的内部(或其友元)中访问。
构造函数
构造函数可以在类声明中实现,可以在类声明外实现,在类声明中实现的构造函数代码如下:
c++
class Human
{
public:
Human()
{
// constructor code here
}
};
在类声明外定义构造函数的代码如下:
c++
class Human
{
public:
Human(); // constructor declaration
};
// constructor implementation (definition)
Human::Human()
{
// constructor code here
}
:: 被称为作用域解析运算符。例如,Human::dateOfBirth 指的是在 Human 类中声明的变量 dateOfBirth,而 ::dateOfBirth 表示全局作用域中的变量 dateOfBirth。
如果你没有写默认构造函数,但是写了重载的构造函数时,C++ 编译器不会生成默认构造函数,这时候创建实例的时候只能调用重载的构造函数。
默认构造函数是调用时可不提供参数的构造函数,而并不一定是不接受任何参数的构造函数。因此,下面的构造函数虽然有两个参数,但它们都有默认值,因此也是默认构造函数:
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 类的析构函数的声明类似于下面这样:
c++
class Human
{
~Human(); // declaration of a destructor
};
析构函数可在类声明中实现,也可在类声明外实现。在类声明中实现(定义)析构函数的代码类似于下面这样:
cpp
class Human{
public:
~Human(){
// destructor code here
}
};
在类声明外定义析构函数的代码类似于下面这样:
cpp
class Human{
public:
~Human(); // destructor declaration
};
// destructor definition (implementation)
Human::~Human(){
// destructor code here
}
正如您看到的,析构函数的声明与构造函数稍有不同,那就是包含腭化符号(~)。然而,析构函数的作用与构造函数完全相反。
每当对象不再在作用域内或通过 delete 被删除进而被销毁时,都将调用析构函数。这使得析构函数成为重置变量以及释放动态分配的内存和其他资源的理想场所。
使用 char* 缓冲区时,你必须自己管理内存分配和释放,因此建议不要使用它们,而使用std::string。std::string 充分利用了构造函数和析构函数,让您无需考虑分配和释放等内存管理工作。
如下所示是一个名为 MyString 的类,在构造函数中为一个字符串分配内存,并在析构函数中释放它。
c++
#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;
}
输出:
vbnet
String buffer in sayHello is 23 characters long
Buffer contains: Hello from String Class
Invoking destructor, clearing up
这样你在使用 MyString::buffer 时就不需要考虑内存的分配和释放了。
拷贝构造函数
有一个函数 Area(),代码如下:
arduino
double Area(double radius);
浅拷贝及其存在的问题
看下面的代码:
c++
#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;
}
运行后输出:
vbnet
String buffer in MyString is 23 characters long
buffer contains: Hello from String Class
Invoking destructor, clearing up
Invoking destructor, clearing up
然后报错了,报错信息如下:
这里在 main() 中调用了函数 UseMyString(),并传入 sayHello 对象,sayHello 被复制给形参 str,因此 sayHello.buffer 和 str.buffer 指向同一个内存单元,函数 UseMyString() 返回时,变量 str 被销毁,此时将调用 MyString 类的析构函数,使用 delete[] 释放分配给 buffer 的内存,同时将导致 sayHello.buffer 指向的内存单元无效,等 main() 执行完毕时,对不再有效的内存地址调用 delete,正是这种重复调用 delete 导致了程序崩溃。
深拷贝
拷贝构造函数接收 以引用的方式传入的当前类的对象 作为参数可以实现深拷贝,其语法如下:
c++
class MyString
{
MyString(const MyString& copySource); // copy constructor
};
MyString::MyString(const MyString& copySource)
{
// Copy constructor implementation code
}
代码如下:
c++
#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);
int num;
cin >> num;
return 0;
}
运行后打印如下:
vbnet
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
可以看到代码跟之前差不多,只是新增了一个拷贝构造函数,拷贝构造函数除了参数跟构造函数不一样,其他代码一模一样。从输出可以看出来,两个 buffer 指向的内存地址不同,函数 UseMyString() 返回、形参 str 被销毁时,析构函数对拷贝构造函数分配的内存地址调用 delete[],不会影响 main() 中 sayHello 指向的内存。因此,这两个函数都执行完毕时,成功地销毁了各自的对象,没有导致应用程序崩溃。
不允许拷贝的类
假设您需要模拟国家的政体。一个国家只能有一位总统,而 President 类面临如下风险:
ini
President ourPresident;
DoSomething(ourPresident); // duplicate created in passing by value
President clone;
clone = ourPresident; // duplicate via assignment
如果您不声明拷贝构造函数,C++将为您添加一个公有的默认拷贝构造函数,要禁止类对象被拷贝,可声明一个私有的拷贝构造函数。这确保函数调用 DoSomething(OurPresident) 无法通过编译。为禁止赋值,可声明一个私有的赋值运算符。代码如下:
c++
class President
{
private:
President(const President&); // private copy constructor
President& operator= (const President&); // private copy assignment operator
// ... other attributes
};
无需给私有拷贝构造函数和私有赋值运算符提供实现,只需将它们声明为私有的就足以实现目标:确保 President 的对象是不可复制的。
单例类
前面讨论的 President 类很不错,但存在一个缺陷:无法禁止通过实例化多个对象来创建多名总统:
ini
President One, Two, Three;
要禁止创建其他的 President 对象,可使用单例的概念,要创建单例类,就要使用 static 关键字,代码如下:
c++
static President& GetInstance()
{
// static objects are constructed only once
static President onlyInstance;
return onlyInstance;
}
int main()
{
President& onlyPresident = President::GetInstance();
}
GetInstance() 是静态成员,类似于全局函数,无需通过对象来调用它。
禁止在栈中实例化类
栈空间是有限的,如果一个类占几 TB 的数据,你需要禁止在栈中实例化它,如何实现呢?你可以把析构函数声明成私有的:
c++
class MonsterDB
{
private:
~MonsterDB(); // private destructor
//... members that consume a huge amount of data
};
这样下面的代码将会编译报错:
c++
int main()
{
MonsterDB myDatabase; // compile error
// ... more code
return 0;
}
因为退栈时将弹出栈中的所有对象,编译器需要在 main() 末尾调用析构函数~MonsterDB(),但这个析构函数是私有的,因此上述语句将编译错误。
将析构函数声明为私有的并不能禁止在堆中实例化:
c++
int main()
{
MonsterDB* myDatabase = new MonsterDB(); // no error
// ... more code
return 0;
}
上述代码不会编译报错,但是会导致内存泄漏。由于在 main() 中不能调用析构函数,因此也不能调用 delete。为了解决这种问题,需要在 MonsterDB 类中提供一个销毁实例的静态公有函数(作为类成员,它能够调用析构函数),代码如下:
c++
#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。
共用体
共用体是一种特殊的类,每次只有一个非静态数据成员处于活动状态。共用体与类一样,可包含多个数据成员,但不同的是只能使用其中的一个。代码让如下:
ini
union UnionName
{
Type1 member1;
Type2 member2;
...
TypeN memberN;
};
要实例化并使用共用体,可像下面这样做:
ini
UnionName unionObject;
unionObject.member2 = value; // choose member2 as the active member
与结构类似,共用体的成员默认也是公有的,但不同的是,共用体不能继承。另外,将 sizeof() 用于共用体时,结果总是为共用体最大成员的长度,即便该成员并不处于活动状态。