本文使用 VS 2010/2015 作为开发环境。
TOC
从结构体开始
以下是一个比较简单的 C++ 程序,实际上,这段代码可以使用 C(而不是C++)编译器完成编译。
//抑制编译器对函数安全性的提示。
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <limits.h>
int main()
{
int value;
printf("%s\n", "请输入一个数字:");
scanf("%d", &value);
if (value > SHRT_MAX || value < SHRT_MIN)
{
printf("%s\n", "数值太大。");
return 1;
}
printf("%d x %d = %d\n", value, value, value * value);
return 0;
}
运行结果如下
请输入一个数字: 12345 12345 x 12345 = 152399025 请按任意键继续. . .
然后,我们开始使用结构体。下面的代码看看结构就可以了,具体的实现方法就不用管了。
//抑制编译器对函数安全性的提示。
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <limits.h>
#include <assert.h>
#define MAX_BOOK_NAME 64
// C 中无 class
struct Book
{
unsigned int Id;
char Name[MAX_BOOK_NAME];
};
// C 中无引用,且所有的结构体在使用时
// 必须以 struct MyStructure 的形式出现。
// 从控制台输入图书信息。
void InputBook(struct Book* book)
{
assert(book != NULL);
puts("Id =");
scanf("%d", &book->Id);
puts("书名 =");
_flushall();
gets(book->Name);
}
// 打印图书信息到控制台。
void PrintBook(struct Book* book)
{
assert(book != NULL);
printf("图书 [%d] %s\n", book->Id, book->Name);
}
int main()
{
struct Book onlyBook;
InputBook(&onlyBook);
puts("");
PrintBook(&onlyBook);
return 0;
}
运行结果如下
Id = 1002300 书名 = The Test 图书 [1002300] The Test 请按任意键继续. . .
C++
接下来,如果使用 C++ 的编写方法,上面的代码可以写成这样
//抑制编译器对函数安全性的提示。
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <limits.h>
#include <assert.h>
// C++ 中的 const 具有真正的常量语义,
// 可用于下面数组维数的声明。
const int MAX_BOOK_NAME = 64;
// C++ 中 class 与 struct 仅存在默认可访问性的差异,
// 其他方面一模一样。
struct Book
{
unsigned int Id;
char Name[MAX_BOOK_NAME];
};
// 从控制台输入图书信息。
void InputBook(Book& book)
{
puts("Id =");
scanf("%d", &book.Id);
puts("书名 =");
_flushall();
gets(book.Name);
}
// 打印图书信息到控制台。
void PrintBook(Book& book)
{
printf("图书 [%d] %s\n", book.Id, book.Name);
}
int main()
{
Book onlyBook;
InputBook(onlyBook);
puts("");
PrintBook(onlyBook);
return 0;
}
运行结果不变,此处不再赘述。
顺带一提,本文中将`Book`结构体中声明的两个变量`Book::Id`和`Book::Name`称为字段[field]。
面向对象
C++最重要的一点就是引入了类[class]的概念以及`class`关键字。为了和C保持兼容性,C++ 中`class`与`struct`仅存在默认可访问性的差异。其实,你正在使用的究竟是结构体还是类,其实是取决于你的使用方式。对于高级语言(如 VB/C#/JAVA)来说,类分配在堆[heap]上,默认是按引用传递(或者说一般用的是对应的指针类型),而结构体分配在栈(堆栈)[stack]上,默认是按值传递。而在C++中,我们可以自由控制结构体/类的分配位置
const int MAX_BOOK_NAME = 64;
struct Book
{
unsigned int Id;
char Name[MAX_BOOK_NAME];
};
int main()
{
//在 main 的调用堆栈上就地创建一个结构体。
//注意一个线程的调用堆栈[call stack]一般比较小,
//例如 Windows 默认是 1MB。
//如果堆栈空间用尽,则会引发栈存溢出[stack overflow]。
Book bookOnStack;
//在堆中创建一个结构体,并在栈中创建一个指针,
//使其指向堆中结构体的地址。
Book* bookOnHeap = new Book();
bookOnStack.Id = 123;
bookOnHeap->Id = 234;
//堆中的对象需要手动删除。
delete bookOnHeap;
return 0;
}
同样,我们也可以自由控制类和结构体的传递方式。此处就不再赘述了。因此,下文中的“结构体”和“类”指代的是同一种数据结构——这与高级语言是不同的。
把函数放到类的里面,成为方法[method]
//抑制编译器对函数安全性的提示。
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <limits.h>
#include <assert.h>
// C++ 中的 const 具有真正的常量语义,
// 可用于下面数组维数的声明。
const int MAX_BOOK_NAME = 64;
// C++ 中 class 与 struct 仅存在默认可访问性的差异,
// 其他方面一模一样。
struct Book
{
unsigned int Id;
char Name[MAX_BOOK_NAME];
// 从控制台输入图书信息。
void InputBook()
{
puts("Id =");
scanf("%d", &this->Id);
puts("书名 =");
_flushall();
gets(this->Name);
}
// 打印图书信息到控制台。
void PrintBook()
{
//在不引起歧义的情况下,this-> 可以省略。
printf("图书 [%d] %s\n", Id, Name);
}
};
int main()
{
Book onlyBook;
onlyBook.InputBook();
puts("");
onlyBook.PrintBook();
return 0;
}
运行结果不变。
由此可以看出,类的引入使得程序的模块化变得更为便利。
设置成员的可访问性
假使我们不希望书的信息可以在 main 中随意修改,像这样
int main()
{
Book onlyBook;
onlyBook.InputBook();
onlyBook.Id = 123456;
puts("");
onlyBook.PrintBook();
return 0;
}
而仅允许用户通过控制台来输入/输出书籍信息,那么我们应当限制 Book 类中成员[member]的可访问性[accessibility]:
struct Book
{
private:
unsigned int Id;
char Name[MAX_BOOK_NAME];
public:
// 从控制台输入图书信息。
void InputBook()
{
puts("Id =");
scanf("%d", &this->Id);
puts("书名 =");
_flushall();
gets(this->Name);
}
// 打印图书信息到控制台。
void PrintBook()
{
//在不引起歧义的情况下,this-> 可以省略。
printf("图书 [%d] %s\n", Id, Name);
}
};
这样,在 Book 类之外的地方就不能访问`Id`和`Name`了。实际上,这一点也可以从VS的自动完成中看到

如果我们使用以下代码
int main()
{
Book onlyBook;
onlyBook.Id = 123;
return 0;
}
则会导致编译器报错
1>\projects\ctest\ctest\main.cpp(41): error C2248: “Book::Id”: 无法访问 private 成员(在“Book”类中声明) 1> \projects\ctest\ctest\main.cpp(17) : 参见“Book::Id”的声明 1> \projects\ctest\ctest\main.cpp(15) : 参见“Book”的声明
也就是说,通过可访问性控制,作为一个功能模块和接口的设计者,我们可以明确地指出,一个成员究竟是这个模块的内部组成部分,还是对外暴露的接口。例如,此处`Book`的定义很明显就是为了让大家通过`InputBook`和`PrintBook`来使用功能的。至于`Id`和`Name`,作为接口的设计者,我们不希望其可以由`Book`类外面的代码直接访问到,因此使用`private`说明其访问性只对类的内部成员可见。
`struct`和`class`
之前有提到过,`class`和`struct`仅存在默认可访问性的区别,那么,它们之间究竟有什么不同呢?我们可以通过两段代码比较一下
| 等价于 | |
struct Book
{
unsigned int Id;
char Name[MAX_BOOK_NAME];
};
|
struct Book
{
public:
unsigned int Id;
char Name[MAX_BOOK_NAME];
};
|
class Book
{
unsigned int Id;
char Name[MAX_BOOK_NAME];
};
|
struct Book
{
private:
unsigned int Id;
char Name[MAX_BOOK_NAME];
};
|
注意:可见性[visibility]和可访问性[accessibility]是两个不同的概念。尽管我们在代码中无法访问到`Id`和`Name`,但任何人只要有`Book`类的定义[definition],就是上面class Book {…};这一部分,都可以看到这个类里面有两个字段。尽管类中的函数可以将函数头(声明)和函数体(定义)分开,但字段是必须留在类的定义中的。实际上,编译器需要这段代码来确定`Book`类的内存布局。例如,sizeof(Book) 的计算结果是 sizeof(Book::Id) + sizeof(Book::Name) = 4 + 64 = 68 :

可见性与二进制兼容性
可见性与可访问性的分裂直接影响了 C++ 库的版本二进制兼容性。因为C++中,所有的成员都是通过内存地址,以及内存地址加上偏移量来确定的。因此,一旦一个类的声明发生了变化,即使发生变化的是私有成员——外界无法访问——但仍会造成整个类的内存布局变化。如果你正在设计一个类库,那么你将要面对由此带来的不同版本之间的兼容性问题。人们为了解决这一问题,提出了诸如 COM、PImpl 之类的设计模式,这里就不再赘述了。一般来说,高级语言不存在这个问题。VB6.0 及其以前版本中,所有的类均继承自`IDispose`和`IDispatch`,使用 COM 实现[这也就是 VB6 中类模块中不能公开结构体(使用Type语句定义)的原因];在 .NET/JAVA 中,成员调用是通过成员名称对应而不是内存地址对应的,因此也不存在二进制兼容性的问题。
内聚性与耦合性
这里使用`Book`类作为示例也存在不妥之处,因为作为一个功能模块,`Book`仅仅用于保存图书信息;然而,很明显,`InputBook`和`PrintBook`的实现中使用到了`printf`和`scanf`等函数,这意味着`Book`对这些函数产生了依赖(耦合)。试想,如果我们准备使用可视化界面而不是控制台来输入/输出信息,那么`InputBook`和`PrintBook`就不可避免地需要做出修改。这显然是不合理的。因此,在设计模块的时候,我们需要尽量提高模块的内聚性,减少模块之间的耦合性。也就是说,尽量让一个模块只做一件事情。
那么,整个代码可以写成这个样子:
//抑制编译器对函数安全性的提示。
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <limits.h>
#include <assert.h>
#include <string.h>
// C++ 中的 const 具有真正的常量语义,
// 可用于下面数组维数的声明。
const int MAX_BOOK_NAME = 64;
// C++ 中 class 与 struct 仅存在默认可访问性的差异,
// 其他方面一模一样。
struct Book
{
private:
unsigned int Id;
char Name[MAX_BOOK_NAME];
public:
// 设置图书信息。
bool Set(unsigned int id, char* name)
{
Id = id;
//进行一个简单的输入检查,
//即检查输入的名称是否太长,
//以至于无法放到 name 中。
if (strlen(name) > MAX_BOOK_NAME)
return false;
strcpy(Name, name);
}
// 向指定的字符串缓冲区中填入图书信息。
void ToString(char* buffer)
{
//这里假定 buffer 分配的内存足以填充所有的内容。
//(否则会发生缓冲区溢出)
sprintf(buffer, "图书 [%d] %s", Id, Name);
}
};
int main()
{
Book onlyBook;
char buffer[128];
unsigned int temp;
onlyBook.Set(0, "Book of Nullity");
onlyBook.ToString(buffer);
puts(buffer);
puts("请按照“序号 书名”的格式键入:");
//这意味着书名不能包含空格。
scanf("%d %s", &temp, buffer);
onlyBook.Set(temp, buffer);
//再显示一次。
onlyBook.ToString(buffer);
puts(buffer);
return 0;
}
如果 main 函数愿意的话,可以使用 STL 中的`iostream`来进行输入/输出
#include <iostream>
using std::cin;
using std::cout;
using std::endl;
int main()
{
Book onlyBook;
char buffer[128];
unsigned int temp;
onlyBook.Set(0, "Book of Nullity");
onlyBook.ToString(buffer);
cout << buffer << endl;
cout << "请按照“序号 书名”的格式键入:" << endl;
//这意味着书名不能包含空格。
cin >> temp >> buffer;
onlyBook.Set(temp, buffer);
//再显示一次。
onlyBook.ToString(buffer);
cout << buffer << endl;
return 0;
}
可以注意到,由于`Book`并未直接依赖stdio,因此我们可以不修改41行以前的所有代码。
