再读高质量C++C 编程指南

 
为了防止头文件被重复引用,应当用 ifndef/define/endif 结构产生预处理块。
 
C++ 语言可以用 const 来定义常量,也可以用 #define 来定义常量。但是前者比后者有更多的优点:
( 1) const 常量有数据类型,而宏常量没有数据类型。编译器可以对前者进行类型安全检查。而对后者只进行字符替换,没有类型安全检查,并且在字符替换可能会产生意料不到的错误(边际效应)。
( 2) 有些集成化的调试工具可以对const 常量进行调试,但是不能对宏常量进行调试。
 
有时我们希望某些常量只在类中有效。由于 #define 定义的宏常量是全局的,不能达到目的,于是想当然地觉得应该用const 修饰数据成员来实现。const 数据成员的确是存在的,但其含义却不是我们所期望的。const 数据成员只在某个对象生存期内是常量,而对于整个类而言却是可变的,因为类可以创建多个对象,不同的对象其const 数据成员的值可以不同。const 数据成员的初始化只能在类构造函数的初始化表中进行。
 
在函数体的“入口处”,对参数的有效性进行检查。
在函数体的“出口处”,对 return 语句的正确性和效率进行检查。
 
常见的内存错误:
u 内存分配未成功,却使用了它。
u 内存分配虽然成功,但是尚未初始化就引用它。
u 内存分配成功并且已经初始化,但操作越过了内存的边界。
u 忘记了释放内存,造成内存泄露。
u 释放了内存却继续使用它。
 
数组要么在静态存储区被创建(如全局数组),要么在栈上被创建。数组名对应着(而
不是指向)一块内存,其地址与容量在生命期内保持不变,只有数组的内容可以改变。
指针可以随时指向任意类型的内存块,它的特征是“可变”,所以我们常用指针来操
作动态内存。指针远比数组灵活,但也更危险。
 
注意当数组作为函数的参数进行传递时,该数组自动退化为同类型的指针。
 
如果函数的参数是一个指针,不要指望用该指针去申请动态内存。编译器总是要为函数的每个参数制作临时副本,指针参数 p 的副本是 _p,编译器使 _p = p 。
 
我们发现指针有一些“似是而非”的特征:
(1)指针消亡了,并不表示它所指的内存会被自动释放。
(2)内存被释放了,并不表示指针会消亡或者成了NULL 指针。
 
malloc/free 是库函数而不是运算符,不在编译器控制权限之内,不能够把执行构造函数
和析构函数的任务强加于 malloc/free
由于内部数据类型的“对象”没有构造与析构的过程,对它们而言 malloc/free new/delete 是等价的。
 
如果在申请动态内存时找不到足够大的内存块, malloc new将返回 NULL 指针,
宣告内存申请失败。通常有三种方式处理“内存耗尽”问题。
(1)      判断指针是否为 NULL,如果是则马上用 return 语句终止本函数。
(2)      判断指针是否为 NULL,如果是则马上用 exit(1)终止整个程序的运行。
(3)      new malloc 设置异常处理函数。
有一个很重要的现象要告诉大家。对于 32 位以上的应用程序而言,无论怎样使用
malloc new,几乎不可能导致“内存耗尽”。我在 Windows 98 下用 Visual C++编写了
测试程序,见示例 7-9。这个程序会无休止地运行下去,根本不会终止。因为 32 位操作
系统支持“虚存”,内存用完了,自动用硬盘空间顶替。我只听到硬盘嘎吱嘎吱地响,
Window 98 已经累得对键盘、鼠标毫无反应。
 
编译器根据参数为每个重载函数产生不同的内部标识符。例如编译器为示例 8-1-1 中的三个Eat 函数产生象_eat_beef、_eat_fish、_eat_chicken 之类的内部标识符(不同的编译器可能产生不同风格的内部标识符)。
 
如果 C++程序要调用已经被编译后的 C 函数,该怎么办?
假设某个 C 函数的声明如下:
void foo(int x, int y);
该函数被 C 编译器编译后在库中的名字为 _foo,而 C++编译器则会产生像 _foo_int_int
之类的名字用来支持函数重载和类型安全连接。由于编译后的名字不同, C++程序不能
直接调用 C 函数。 C++提供了一个 C 连接交换指定符号 externC”来解决这个问题。
这就告诉 C++编译译器,函数 foo 是个 C 连接,应该到库中找名字 _foo 而不是找
_foo_int_intC++编译器开发商已经对 C 标准库的头文件作了 externC”处理,所以
我们可以用# include 直接引用这些头文件。
 
注意并不是两个函数的名字相同就能构成重载。全局函数和类的成员函数同名不算
重载,因为函数的作用域不同
 
成员函数的重载、覆盖( override)与隐藏很容易混淆,C++程序员必须要搞清楚概
 
参数缺省值只能出现在函数的声明中,而不能出现在定义体中。
如果函数有多个参数,参数只能从后向前挨个儿缺省,否则将导致函数调用语句怪模怪样。
 
运算符与普通函数在调用时的不同之处是:对于普通函数,参数出现在圆括号内;而对于运算符,参数出现在其左、右侧。
如果运算符被重载为全局函数,那么只有一个参数的运算符叫做一元运算符,有两个参数的运算符叫做二元运算符。
如果运算符被重载为类的成员函数,那么一元运算符没有参数,二元运算符只有一个右侧参数,因为对象自己成了左侧参数。
 
 
构造函数初始化表的使用规则:
u 如果类存在继承关系,派生类必须在其初始化表里调用基类的构造函数。
u 类的 const 常量只能在初始化表里被初始化,因为它不能在函数体内用赋值的方式
来初始化。
u 类的数据成员的初始化可以采用初始化表或函数体内赋值两种方式,这两种方式的
效率不完全相同。
 
一个有趣的现象是,成员对象初始化的次序完全不受它们在初始化表中次序的影响,只由成员对象在类中声明的次序决定。这是因为类的声明是唯一的,而类的构造函数可以有多个,因此会有多个不同次序的初始化表。如果成员对象按照初始化表的次序进行构造,这将导致析构函数无法得到唯一的逆序。
 
不要轻视拷贝构造函数与赋值函数
u 本章开头讲过,如果不主动编写拷贝构造函数和赋值函数,编译器将以“位拷贝”
的方式自动生成缺省的函数。倘若类中含有指针变量,那么这两个缺省的函数就隐含了错误。
u 拷贝构造函数和赋值函数非常容易混淆,常导致错写、错用。拷贝构造函数是在对象被创建时调用的,而赋值函数只能被已经存在了的对象调用。
以下程序中,第三个语句和第四个语句很相似,你分得清楚哪个调用了拷贝构造函数,哪个调用了赋值函数吗?
String a( hello );
String b( world );
String c = a; // 调用了拷贝构造函数,最好写成 c(a);
c = b; // 调用了赋值函数
本例中第三个语句的风格较差,宜改写成 String c(a) 以区别于第四个语句。
 
如果我们实在不想编写拷贝构造函数和赋值函数,又不允许别人使用编译器生成的缺省函数,怎么办?偷懒的办法是:只需将拷贝构造函数和赋值函数声明为私有函数,不用编写代码。
 
基类的构造函数、析构函数、赋值函数都不能被派生类继承。如果类之间存在继承关系,在编写上述基本函数时应注意以下事项:
u 派生类的构造函数应在其初始化表里调用基类的构造函数。
u 基类与派生类的析构函数应该为虚(即加 virtual 关键字)。
u 在编写派生类的赋值函数时,注意不要忘记对基类的数据成员重新赋值。
 
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值