漫谈

本文使用 VS 2010/2015 作为开发环境。

小提示:在 Visual Studio 中,为了使得直接运行(不调试)控制台程序时,能够在控制台窗口的最后显示“请按任意键继续”,需要在“解决方案资源管理器”中打开项目的属性页面,然后在 配置属性->链接器 -> 系统 -> 子系统 中选择“控制台 (/SUBSYSTEM:CONSOLE)”。

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的自动完成中看到

从自动完成列表中可以看到,Id 和 Name 均为私有成员,对外是不可访问的。
从自动完成列表中可以看到,Id 和 Name 均为私有成员,对外是不可访问的。

如果我们使用以下代码

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 :
zephyr-1-book-size

可见性与二进制兼容性

可见性与可访问性的分裂直接影响了 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行以前的所有代码。

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注

ERROR: si-captcha.php plugin: GD image support not detected in PHP!

Contact your web host and ask them to enable GD image support for PHP.

ERROR: si-captcha.php plugin: imagepng function not detected in PHP!

Contact your web host and ask them to enable imagepng for PHP.

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据

Content is available under CC BY-SA 3.0 unless otherwise noted.