不常见的数据类型有:
- 结构体
- 指针
- 全局数据
结构体:
使用结构体的理由:
v 用结构体来明确数据关系
v 用结构体简化对数据块的操作
v 用结构体来简化参数列表
v 用结构体来减少维护
指针:
指针的使用是现代编程中最容易出错的领域之一。对指针的运用具有其固有的复杂性,很多常见的安全问题,特别是缓冲区溢出,其产生都可以追溯到错误运用指针上去。
用来理解指针的范例:
从概念来看,每一个指针都包含2个部分:内存中的某处位置+如何解释该位置的内容
内存中的位置:
内存中的一个位置就是一个地址,常用16进制形式表示。32位处理器中的一个地址用一个32位的值表示,如0x0001EA40。指针本身只包含这个地址。为了使用该指针所指向的数据,就必须访问该地址,解释该位置的内存内容。如果去查看改地址的内存,可以发现它只是一组二进制位。必须经过解释才能使它变得有意义。
如何解释指针所指的内容
如何解释内存中某个位置的内容,是由指针的基类型(base type)决定的。如果某指针指向整数,这就意味着编译器会把该指针所指向内存位置的数据解释为一个整数。当然,你可以让一个整数指针、一个字符串指针和一个浮点数指针都指向同一个内存位置。但是其中(至多)只有一个指针能正确地解释该位置的内容。
在理解指针的时候,应该记住内存并不包含任何与之相关联的内在的解释。只有通过使用一个特定类型的指针,一个特殊位置的比特才能解释为有意义的数据。
示例:
显示对内存中同一位置的几种不同观点,用几种不同的方式做出解释:
0A |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
6A |
解释方式:用于进一步举例的原始内存空间(用16进制表示)
意义:没有与之关联的指针变量,没有任何意义。
0A |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
6A |
解释方式:String[10]
意义:abcdefghij
0A |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
6A |
解释方式:双字节的整数
意义:24842
0A |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
6A |
解释方式:四字节的浮点数
意义:4.17595656202980E+0021
0A |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
6A |
解释方式:四字节的整数
意义:1667391754
0A |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
6A |
解释方式:字符
意义:换行符
注:上图中,各数据类型所用的内存量用红字表示
小结:在上图的每种情况中,指针指向的都是以16进制数值0x0A开始的位置。0A之后使用的字节数量取决于这片内存的解释。内存内容如何使用也要取决于内存的解释方式。同样的原始内存空间可以解释为一个字符串、一个整数、一个浮点数,或者任何其它事物--一切都取决于指向该内存的指针的基类型。
使用指针的一般技巧:
内存破坏(memory corruption):通过一个坏了的指针变量赋值时,把数据写入本不该写值的内存区域。
后果--
u 有时会导致可怕、严重的系统崩溃
u 有时会篡改其它部分的计算结果
u 有时会致使你的程序不可预知的跳过某些子程序
u 有时它又什么事情都没有做
使用技巧:
n 把指针操作限制在子程序或类里面 -- 假设你的程序中多次使用了一个链表,每次使用它时,不要通过手动操作指针去遍历该链表,应该编写一组诸如NextLink()、PreviousLink()、InsertLink()和DeleteLink()这样的访问器子程序来完成操作更好些。
n 同时声明和定义指针 -- 在靠近变量声明的位置为该变量赋初始值通常是一项好的编程实践,在使用指针时,应用这条原则会更有价值。
示例
槽糕的指针初始化:
Employee *employeePtr;
// lots of code
...
employeePtr = new Employee;
良好的指针初始化:
// lots of code
...
Employee *employeePtr = new Employee;
n 在与指针分配相同的作用域中删除指针--要保持指针分配和释放操作的对称性
n 在使用指针之前检查指针--在程序的关键之处使用一个指针之前,要确保它所指向的内存位置是合理的。
n 先检查指针所引用的变量再使用它--有时你应该对指针所指向的数据执行合理性检查。
n 用狗牌字段来检测损毁的内存--”标记字段(tag field)“或者”狗牌(dog tag)“是指你加入结构体内的一个仅仅用于检测错误的字段。在分配一个变量的时候,把一个应该保持不变的数值放在它的标记字段里。当你使用该结构体的时候,特别是当你释放内存的时候,检测这个标记字段的取值。若该标记字段的取值与预期不符,那么这一数据就被破坏了。
n 增加明显的冗余 --代替标记字段的方案:将某些特定字段重复两次,如果位于冗余字段中的数据不匹配,你就可以确定数据已经破坏了。
n 用额外的指针变量来提高代码清晰度--一定不要节约使用指针变量。
示例:
传统的插入节点的代码(两个对象):
void InsertLink(Node *currentNode,Node *insertNode)
{
// insert “insertNode” after “currentNode”
insertNode->next = currentNode->next;
insertNode->previous = currentNode;\
If(currentNode->next != NULL){
currentNode->next->previous = insertNode;
}
currentNode->next = insertNode;
}
更具可读性的节点插入代码(三个对象):
void InsertLink(Node *startNode, Node *newMiddleNode)
{
// insert “newMiddleNOde” between “startNode” and “followingNode”
Node *followingNode = startNOde -> next;
newMiddleNode->next = followingNode;
newMIddleNode->previous = startNode;
If(followingNode != NULL){
followingNode->previous = newMIddleNode;
}
startNode->next = newMiddleNode;
}
n 简化复杂的指针表达式
n 按照正确的顺序删除链表中的指针--在使用动态分配链表时,经常遇到的一个问题是,如果先释放了链表中的第一个指针,就会导致表中的下一个指针无法访问。为了避免这一问题,在释放当前指针之前,要确保已经有指向链表中下一个元素的指针。
n 分配一片保留的内存后备区域--如果使用了动态内存,就需要避免发生忽然用尽了内存、把你的用户和用户数据丢在RAM空间的尴尬场景。设法确认程序为了”保存所做的工作,执行清理并体面地退出“需要多少内存。在程序初始化阶段把这部分内存分配出来作为后备区,执行清理工作,然后退出。
n 粉碎垃圾数据--指针错误是很难调试的,因为你无法确定指针所指向的内存何时变成非法的。有的指针已经释放了很长一段时间,相应内存的内容看起来还像是有效的。而在另一些时候,这些内存内容马上就会改变。
示例(C++强制让释放的内存包含垃圾数据):
memset(pointer, GARBAGE_DATA, MemoryBlockSize(pointer));
delete pointer;
n 在删除或者释放指针之后把它们设为空值--一种常见的指针错误是”悬空指针(dangling pointer)“,即使用一个已经被delete或者free的指针。指针错误难于检测的原因之一就是,这类错误有的时候并不产生任何征兆。尽管在删除指针后再将其设为null并不能阻止你去读取一个空悬指针所指向的数据,但这的确可以保证当你向一个空悬指针写入数据时会产生错误。
示例(在删除指针后将其设为NULL):
memset(pointer, GARBAGE_DATA, MemoryBlockSize(pointer));
delete pointer;
Pointer = NULL;
n 在删除变量之前检查非法指针--破坏程序的最好办法之一就是在已经删除或者释放了一个指针之后再delete()或者free()它。PS:很少有语言能够检测出这类问题来。 如果把已经释放的指针设置为空,就可以在使用或者试图再度删除指针之前对其进行检测。如果不把已释放的指针设为空值,就不可能拥有这项选择。这就提出了对前面删除指针的代码另一扩充:
示例(在删除指针之前断言其不为NULL):
ASSERT(pointer != NULL, “Attempting to delete null pointer.”);
memset(pointer, GARBAGE_DATA, MemoryBlockSize(pointer));
delete pointer;
pointer = NULL;
n 跟踪指针分配情况--维护一份你已经分配的指针的列表,在释放指针前可以检查是不是位于列表中。
示例(检查是否已经分配了某个指针):
ASSERT(pointer != NULL, ”Attempting to delete null pointer.“);
If(IsPointerInList(pointer)){
memset(pointer, GARBAGE_DATA, MemoryBlockSize(pointer));
RemovePointerFromList(pointer);
delete pointer;
pointer = NULL;
}
else{
ASSERT(FALSE, “Attempting to delete unallocated pointer.”);
}
n 编写覆盖子程序,集中实现避免指针问题的策略--避免应用多种冲突的策略,通常对常用的指针操作编写覆盖子程序(cover routine),可以减少编程的工作量,并且降低错误几率。C++ 可用如下两个子程序:
1) SAFE_NEW 这个子程序调用new来分配指针,把这一新的指针加入已分配指针列表中,然后将这一新分配的指针返回给调用方子程序。它还可以在该子程序内检查new操作的返回值是否为空或是否抛出异常(也就是是否发生”内存不足“错误),从而简化了程序其他部分的错误处理。
2) SAFE_DELETE 这个子程序检查传递给它的指针是否在已分配指针的列表里。如果它在列表里,就把该指针所指向的内存设置为垃圾数值,把该指针从列表中移除,再调用C++的delete运算符释放该指针,并且把该指针设为空值。如果该指针不在列表里,那么SAFE_DELETE将显示一条诊断信息,并且终止程序运行。
示例(用宏实现SAFE_DELETE子程序)在删除指针的代码外加一层包裹:
#define SAFE_DELETE(pointer) {
ASSERT(pointer != NULL, “Attempting to delete null pointer.”);
If(IsPointerInList(pointer)){
memset(pointer,GARBAGE_DATA, MemoryBlockSize(pointer));
RemovePointerFromList(pointer);
delete pointer;
pointer = NULL;
}
else{
ASSERT(FALSE, “Attempting to delete unallocated pointer.”);
}
}
n 采用非指针的技术--使用能替代指针的工作合理的方案。
使用指针的指导原则
a)C++指针
1. 理解指针和引用之间的区别--指针*和引用&都能间接地引用对象。对新手而言,区别仅在于字面上的object->field和object.field不同。最重要的区别在于--引用必须总是引用一个对象,而指针可以指向空值;而且引用所指向的对象在该引用初始化之后不能改变。、
2. 把指针用于”按引用传递“参数,把const引用用于”按值传递“参数--C++向子程序传递参数的默认方式是值传递(pass by value)【不修改传入的对象,创建多份拷贝会消耗大量的时间和其他资源】,而不是传递引用(pass by reference)。
综合:具有”传值“的语义(即不能修改传入的对象),却用”传引用“的方式来实现(即传递对象本身而非它的拷贝) ----使用指针来实现按引用传递
示例:
void SomeRoutine(
const LARGE_OBJECT &nonmodifiableObject,
LARGE_OBJECT *modifiableObject
);
3. 使用auto_ptr --通过在离开作用域的时候自动释放内存,auto_ptr能避免很多与常规指针相关的内存泄露问题。
4. 灵活运用智能指针--智能指针(smart pointers)是常规指针或”哑(dumb)“指针的一种替代品,针对资源管理、拷贝操作、赋值操作、对象构造和对象析构提供了更多控制。
b)C指针
1. 使用显示指针类型而不是默认类型--C允许你对任何类型的变量使用char或void指针。C语言只关心这类指针有所指向,不会真正去关心它所指向的是什么。然而,如果你使用了显示的指针类型,编译器就会针对不相符的指针类型和不合适的解除引用(dereferences)发出警告。
2. 避免强制类型转换
3. 遵循参数传递的星号规则--在C语言中,只有当你在赋值语句的参数前面加了星号(*),才能把该参数从子程序中传回去。
示例:
- 不奏效的参数传递:
void TryToPassBackAValue(int *parameter){
parameter = SOME_VALUE;}
- 奏效的参数传递:
void TryToPassBackAValue(int *parameter){
*parameter = SOME_VALUE;
}
4. 在内存分配中使用sizeof()确定变量的大小
全局数据
与全局数据有关的问题:
1) 无意间修改了全局数据--”副作用“:无意间在某处修改了一个全局变量的值,错误地认为它在其他的位置还保持不变。
2) 与全局数据有关的奇异的和令人激动的别名问题--”别名“:2个或者更多不同名字,说的是同一个变量。当一个全局变量被传递给一个子程序,然后该子程序将它既用作全局变量又用作参数使用的情况下,就会出现该情况。
3) 与全局数据有关的代码重入问题--多线程访问问题
4) 全局代码阻碍代码重用
5) 与全局数据有关的非确定的初始化顺序事宜
6) 全局数据破坏了模块化和智力上的可管理性
使用全局数据的理由
1. 保存全局数值--用于整个程序的数据。
Ø 表示程序状态的变量,如交互模式、命令行模式、正常模式或者错误恢复模式等的模式标识
Ø 整个程序里面要用到的信息,如每一个子程序都会用到的数据表
2. 模拟具名常量
3. 模拟枚举类型--可以在python等不直接支持枚举类型的语言里用全局变量来模拟枚举变量
4. 简化对极其常用的数据的使用
5. 消除流浪数据--”流浪数据“:有一个在每个子程序里都使用的错误处理对象,当调用链中间的子程序并不使用这一对象的时候,这一对象就被称为”流浪数据“。
只有万不得已时才使用全局数据
v 把每一个变量设置为局部的,仅当需要时才把变量设置为全局的--先设置为子程序内部的局部变量->转变为类里的private或protected变量->全局变量
v 区分全局变量和类变量
v 使用访问器子程序
用访问器子程序来取代全局数据
v 访问器子程序的优势:
1) 对数据的集中控制
2) 对变量的所有引用都得到了保护
3) 自动获得信息隐藏的普遍益处
4) 访问器子程序可以容易地转变为抽象数据类型
v 如何使用访问器子程序
把数据隐藏到类里面。用static关键字或者它的等价物来声明该数据,以确保只存在该数据的单一实例。
1) 要求所有的代码通过访问器子程序来存取数据--可以给所有的全局数据冠以g_前缀,并且除了该变量的访问器子程序以外,所有的代码都不可以访问具有g_前缀的变量。其他全部代码都通过访问器子程序来存取该数据。
2) 不要把所有的全局数据扔在一处--考虑每个全局数据属于哪个类,然后把该数据和它的访问器子程序以及其他的数据和子程序打包放入那个类里面。
3) 用锁定来控制对全局变量的访问--
4) 在访问器子程序里构建一个抽象层
5) 使得对一项数据的所有访问都发生在同一个抽象层上
如何降低使用全局数据的风险
1. 创建一种命名规则来突出全局变量
2. 为全部的全局变量创建一份注释良好的清单
3. 不要用全局变量来存放中间结果
4. 不要把所有的数据都放在一个大对象中并到处传递,以说明你没有使用全局变量
总结: