C++ 高质量程序设计指南读书笔记
第四章 入门
1,全局变量的初始化不要依赖另一个全局变量。因为无法判断顺序。
2,每一个源代码文件就是一个最小的编译单元,每一个编译单元都能独立编译而不需要知道其他编译单元的存在及其编译结果。好处:公开接口、隐藏实现,减少代码修改后重新编译的时间。
3,一个低级数据类型对象总是优先转换为能够容纳的下它的最大值的、占用内存最少的高级类型对象。
4,for循环中,如果计数器从0开始计数,则建议for语句的循环控制变量的取值采用前闭后开区间,这样更直观。for(int i = 0;I < N;i++)循环N次。
5,for循环遍历多维数组。C++中二维数组以先行后列存储在连续内存中,先列后行效率高,因为外层循环走过每一行,在走到特定行时,内层循环走过该行包含的每一列的所有元素,显然内层循环执行时地址跳跃的幅度很小,都是连续地依次访问每一个内存位置相邻的元素,没有跨行存取。
第五章 常量
1,常量分为#define定义的宏常量和const定义的常量。#define是预编译伪指令,在编译阶段前就替换为所代表的字面常量了,因此宏常量在本质上是字面常量。C语言中,const常量是值不能修改的变量,有存储空间。但是C++中,基本数据类型的const常量在符号表中,不分配存储空间,ADT/UDT的const对象会分配存储空间。const常量有数据类型宏常量没有数据类型,编译器会对const常量进行类型检查,不会对宏常量进行类型检查,const常量可以调试,宏常量不可调试。
2,在C语言中,const常量是外连接的,也就是不能在两个编译单元中同时定义一个同名的const常量,或者把一个const常量定义在一个头文件而在多个编译单元同时包含该头文件。但在C++中,const常量是内连接的,编译器会认为它们是不同的符号常量,为每个编译单元分别分配存储空间。不要在头文件中初始化字符串常量,这样包含了这个头文件的每一个编译单元会为那一长串的字符串字面常量创建一个独立的拷贝项,空间开销很大。
3,常量分为:#define常量,const常量,enum常量。
4,类中的cosnt: const数据成员 只在某个对象生存期内是常量,而对于整个类而言却是可变的。因为类可以创建多个对象,不同的对象其const数据成员的值可以不同。所以不能在类的声明中初始化const数据成员,因为类的对象没被创建时,编译器不知道const数据成员的值是什么。const常量只能在初始化列表初始化。但是static const常量可以直接在头文件初始化。
第六章 函数
1,函数调用方式有三种:1,过程式调用,2,嵌套调用,如;lcm(lcm(a,b),c).3,递归调用:自己调自己。
2,参数传递方式:1,值传递 2,地址传递 3,引用传递。如果是指针传递且仅作为输入用,则应在类型前加const,防止指针指向的内存单元被无意修改。如果是以值传递的方式传递对象,则宜改用const &来传递,这样不会调用对象的构造和析构函数,提高效率。
3,返回值。
A, 不要省略返回值类型,如果没有返回值,应声明为void类型。
B, 如果函数的返回值时一个对象,有些场合下可以用“返回引用”替换“返回对象值”,以提高效率,而且还可以支持链式表达,但有些场合只能返回对象值。
例如重载运算符= 和 +,=返回引用,没有内容复制的过程,+返回对象值,函数内创建一个临时对象temp,如果返回引用,在函数结束后引用会失效。
C, return语句不可返回指向堆栈内存的指针或引用,因为该内存单元在函数体结束时会自动释放,如char str[] = “haha”; return str.但是const char *p=”hahaha”;return p,是对的,因为字符串常量保存在程序的静态数据区。
D,return String(s1 + s2)和String result(s1 + s2);return result;不一样:后者是先创建result对象,调用构造函数初始化,再调用拷贝构造函数把result复制到保存返回值的外部存储单元,最后在函数结束时销毁result。前者创建一个临时对象并返回它,编译器可以直接把临时对象创建并初始化在外部存储单元,省去了拷贝和析构。
4,当局部变量与某一全局变量同名时,在函数内部将遮蔽该全局变量,这时可以通过一元作用域解析运算符(::)来引用全局变量,如,::g_iCount++。
5,任何能用递归函数实现的解决方案都可以用迭代来实现,但是对于某些复杂的问题,将递归方案展开为迭代方案可能比较困难,而且程序的清晰性会下降。递归因为反复调用函数占用了大量的堆栈空间,运行时开销很大,但是迭代只发生在一个函数内部,反复使用局部变量进行计算,开销要好很多,但是递归更直观,易于阅读和理解。
6,Use const whenever you need!
第七章 指针 数组 字符串
1,指针的算术运算。
++,--,引用&,解引用*,==,!=。
指针加减一个正整数i,含义不是在其值上直接加减i,还要包含指针所指对象的字节数信息,如:int *pInt = new int[100];pInt +=50;编译器改写为pInt += 50 * sizeof(int)。所以void*类型的指针不能参与算数运算。还有不能对voiud*类型的指针使用*来获取它指向的变量。
2,数组不能从函数的return语句返回,但是数组可以作为函数的参数。数组名其实就是这个数组的首地址。把数组作为函数参数传递给函数时并非把整个数组的内容传递进去,此时数组会退化成一个同类型的指针。
3,字符数组是元素为字符变量的数组,字符串则是以’/0’结束字符的字符数组。
如果用一个字符串字面常量来初始化一个字符数组,数组的长度至少要比字符串长度大1,因为还要保存/0.如char array[] = “Hello”;数组的元素其实是{‘H’,’e’,’l’,’l’,’o’,’’/0}。
4,引用和指针
A, 引用在创建的同时必须初始化,而指针在定义的时候不必初始化,可以在定义后面的任何地方重新赋值。
B, 不存在NULL引用,引用必须与合法的存储单元关联,而指针可以为NULL。
C, 引用一旦被初始化为指向一个对象,它就不能被改变为另一个对象的引用,而指针可以任何时候改变为指向另一个对象。
D,引用的创建和销毁不会调用类的拷贝构造函数和析构函数。
E, 引用主要用途是用来修饰函数的形参和返回值,既有指针的效率,又具有变量使用的方便些和直观性
第八章 高级数据类型、
1,数组作为函数参数时,会自动转换为指针,但是包装在struct/class中的数组,内存空间完全属于该struct/class对象。如果把struct/class对象传递给函数时,其中的数组将全部复制到函数堆栈中,所以struct/class有数组成员时,最好使用指针或者引用传递该对象。
第九章 编译预处理
第十章 文件结构和程序版式
第十一章 命名规则
第十二章 面向对象
1,虚函数
每一个具有虚函数的类叫做多态类,C++编译器必须为每一个多态类至少创建一个虚函数表vtable,它其实就是一个函数指针数组,其中存放着这个类所有的虚函数的地址及该类的类型信息。
2,override和隐藏
派生类定义中的名字(对象或函数名)将义无反顾地隐藏掉基类中任何同名的对象或函数。基于这样的原则,如果派生类定义了一个与其基类的虚函数同名的虚函数,但是参数列表有所不同,那么这就不会被编译器认为是对基类虚函数的改写(Override),而是隐藏,所以也不可能发生运行时绑定。要想达成运行时绑定的效果,派生类和基类中同名的虚函数必须具有相同的原型。
如:
class IRectangle
{
Virtual ~IRectangle(){}
Virtual void Draw() = 0;
}
Class RectangleImpl : public IRectangle
{
…
Virtual void Draw(int scale){cout << “RectangleImpl::Draw(int)”<<endl;}
Virtual void Draw(){cout << “RectangleImpl::Draw()”<<endl;}
}
Void main(void)
{
IRectangle *pRect = IRectangle::CreateRectangle();
pRect->Draw(); //(1)
pRect->Draw(200); //(2)
}
上述1调用由于pRect的静态类型为IRectangle*,所以使用IRectangle::Draw()执行静态类型检查,但由于pRect指向的对象实际是RectangleImpl对象,因此将绑定到RectangleImpl::Draw()。2调用,因为IRectangle没有这个原型的函数,所以拒绝编译,除非pRect的类型为RectangleImpl*。
如果RectangleImpl不重定义Draw():
RectangleImpl* pRectImpl = new RectangleImpl;
pRectImpl->Draw(); //(3)
pRectImpl->Draw(200); //(4)
上述3调用无法编译,因为Draw()被Draw(int)隐藏了。
3,C++支持运行时多态的手段有两种,一种是虚函数机制,另一种就是RTTI。
4,多态数组
如果能够在数组里放置一些多态对象的话,就可以通过一致的接口来动态地调用他们自定义的虚函数实现了。这个想法是好的,但是会有没意识到的问题。如:
Shape a();
Circle b();
Rectangle c();
Shape myShapes[3];
myShapes[0] = a;
myShapes[1] = b;
myShapes[2] = c;
for(int I = 0;i<3;++i)
{
myShapes[i].Draw();
}
数组不会一次调用Shape::Draw(),Circle::Draw(),Rectangle::Draw(),因为数组的元素类型是Shape,不是Shape&或Shape*,所以会按照Shape的大小来分配内存空间,所以三者都会调用Shape::Draw()。
所以不要在数组中直接存放多态对象,而是换之以基类指针或者基类的只能指针。
第十三章 对象的初始化 拷贝 析构
1, 初始化列表
a) 如果类存在继承关系,派生类可以直接在初始化列表里调用基类特定的构造函数以向它传递参数:
Class A
{
A(int x);
}
Class B : public A
{
B(int x,int y);
}
B::B(int x,int y) : A(x)
{}
b) 类的非静态const数据成员和引用成员只能在初始化列表初始化,因为它们存在初始化语义,而不存在复制语义。
c) 如果一个类有另一个类的成员变量,那么在初始化列表和在构造函数里复制的效率是不一样的,用初始化列表直接调用另一个类的拷贝构造函数,在构造函数里复制先调用这个类的默认构造函数创建这个对象,再调用这个类的赋值函数,显然在初始化列表初始化效率更高。
2, 拷贝构造函数 拷贝赋值函数
如果不主动编写拷贝构造函数和拷贝赋值函数,编译器将以“按成员拷贝”的方式自动生成相应的默认函数,如果类中有指针成员或引用成员,那这两个默认函数可能隐含错误。如:类string的两个对象a,b,假设a.m_data的内容为“Hello”,b.m_data的内容为“world”,将a赋值给b,默认赋值函数的“按成员拷贝”意味着执行b.m_data = a.m_data,这将造成三个错误:1,b.m_data原来的内存没有被释放,造成内存泄露。2,b.m_data和a.m_data指向同一块内存,a或b任何一方的变动都会影响另一方。3,在对象被析构,m_data被delete了两次。
拷贝构造函数是在对象被创建并用另一个已经存在的对象来初始化它时调用的,而赋值函数只能把一个对象赋值给另一个已经存在的对象:
String a(“hello”);
String b(“world”);
String c = a; //拷贝构造函数,因为c这时才被创建,不过最好写成c(a)
C = a; //赋值函数,因为c这个对象已经存在。
3, 派生类的基本函数
1, 派生类的构造函数应在初始化列表显示的调用基类的构造函数。
2, 如果基类是多态类,那么必须把基类的析构函数定义为虚函数,这样就可以像其他虚函数一样实现动态绑定,否则可能造成内存泄露。
#include <stdafx.h>
#include <iostream>
class Base
{
public:
Base() {}
~Base() { std::cout << "Base::~Base()" << std::endl; }
};
class Derived : public Base
{
public:
Derived() {}
~Derived() { std::cout << "Derived::~Derived()" << std::endl; }
};
int main(void)
{
Base* p = new Derived();
delete p;
return 0;
}
如果基类的析构函数不是虚函数,派生类的析构函数就不会调用,造成内存泄漏,上述输出为Base::~Base(),将析构函数改为虚函数后,输出为:Derived::~Derived()
Base::~Base().
3, 在编写派生类的赋值函数时,注意不要忘记对基类的数据成员重新赋值,可以通过调用基类的赋值函数来实现。如Base::operator=(other)。因为不能直接操作基类的私有成员。
4,对象的构造和析构次序
首先调用每一个基类的构造函数,然后调用成员对象的构造函数,而每一个基类的构造函数又将首先调用它们各自基类的构造函数,直到最根类。所以,任何一个对象总是首先构造最根类的子对象,然后逐层向下扩展,直到把整个对象构造起来。
析构函数会严格按照与对象构造相反的次序执行,数据成员的初始化次序跟它们在初始化列表中的顺序没有关系,只跟它们在类中声明的顺序有关。显然,如果每个构造函数的初始化列表各成员的顺序不可能完全相同,如果按照这个顺序,那析构函数就不会得到唯一的逆序了。
第十四章 函数的高级特性
1, 重置 覆盖(override) 和隐藏
重载:具有相同的作用域、函数名字相同、参数类型、顺序或数目不同。
覆盖:派生类重新实现了基类的成员函数。不同的作用域,函数名称相同,参数列表相同,基类函数是虚函数。
虚函数的覆盖有两种方式:完全重写和扩展。扩展指派生类虚函数首先调用基类的虚函数,然后再增加新的功能,完全重写则不调。
隐藏:a)派生类的函数与基类的函数同名,但是参数列表有差异,此时,不论有无virtual关键字,基类的函数在派生类中都将被隐藏。b)派生类的函数与基类的函数同名,参数列表也相同,但是基类函数没有virtual关键字,此时,基类的函数在派生类中将被隐藏。
隐藏这个玩意 哎。
2, i++ ++i
int b = ++a;
相当于:
a += 1;
int b = a;i
int b = a++;
相当于:
Int temp = a;
a += 1;
int b = temp;
temp.~int();
3,内联函数
用内联函数替代宏:
A) 宏容易出错,不可调试。
B) 内联函数可以调试,在程序的Debug版本,没有真正的内联,编译器像普通函数那样为它生成含有调试信息的可执行代码,在Release版本才真正内联。
C) 宏不能操作类的私有成员。
D)内联函数以代码膨胀为代价,省去了函数调用的开销,从而提高程序的执行效率,如果函数体的代码比函数调用的开销大得多,那inline的收益会很小。
E) 以下情况不宜用inline:
如果函数体内代码过长,使用内联将导致代码膨胀过大。
如果函数体内出现循环或其他复杂的控制结构,那执行函数体内代码的时间比函数调用的开销大得多,内联的意义不大。
4,类型转换函数
A) 类的构造函数
Class Point2D : public Point
{
Public:
Point2D(const Point& p);
}
Point2D的构造函数可以将一个Point对象自动转换为一个Point2D对象。
B) 自定义类型转换运算符
类型转换运算符以operator关键字开始,紧接着目标类型名和()。
Class Point
{
Public:
Point(float x);
Operator float() const
{return m_x;}
Private:
Float m_x;
}
Point p(100.25);
Coust << p << endl;
C) 类型转换运算符
static_cast<dest_type>(src_obj)
const_cast<dest_type>(src_obj)
reinterpret_cast<dest_type>(src_obj)
dynamic_cast<dest_type>(src_obj)
5,const 成员函数
任何不会修改数据成员的成员函数都应该声明为const,当编写const成员函数时不小心写下了试图修改数据成员的代码,或者调用了非const成员函数,编译器将指出错误。Const跟在函数尾巴。
第十五章 异常处理和RTTI
1, RTTI
A) typeid
if(typeid(device) == typeid(Television))
{
Television *p = static_cast<Television*>(&device);
}
B) dynamic_cast<>
Television& tv = dynamic_cast<Television&>(device);
第十六章 内存管理
1, 内存分配方式
A) 静态存储区域分配。内存在程序编译的时候就已经分配好了,这些内存在程序的整个运行期间都存在,如全局变量、static变量。
B) 在堆栈分配。函数内的局部变量创建在堆栈上,函数结束时这些存储单元自动释放。
C) 堆或自由存储空间分配,也叫动态分配。使用malloc new申请的内存,程序员自己掌握释放内存的时机,用free delete。
D)原则:如果使用堆栈存储和静态存储就能满足应用要求,就不要使用动态存储。
2, 常见的内存错误
A) 内存分配未成功,却使用它。
在使用内存之前判断指针是否为NULL,如果是new申请的内存,用捕获异常来处理。
B) 内存分配成功,但是还没有初始化就使用了它。
C) 内存分配成功并且已经初始化,但操作越过了内存的边界。
D)忘记释放内存或者只释放了部分内存。这样会造成内存泄露。
E) 释放了内存却还在使用它。
3, 指针参数如何传递内存
A)
Void GetMemory(char*,int num)
{
P = (char*)malloc(sizeof(char) * num);
}
编译器总是为函数的每个参数制作临时副本,指针参数p的副本是_p,编译器使_p=p。如果函数修改了_p指向的内容,那么p指向的内容也就被修改了,这就是指针可以作为输出参数的原因。但是这里,_p申请了内存空间并指向它,但是p没有指向它,所以GetMemory并不会输出任何东西,反而会造成内存泄露,因为没有调用相应的free来释放内存。
B)使用指向指针的指针或指针的引用来实现在函数中动态分配内存
Void GetMemory(char **p,int num) //或者char *&p
{
*p = (char*)malloc(sizeof(char) * num);
}
参数为指向指针的指针,编译器会创建一个_p副本,跟p指向同一个指针,函数内*_p跟*p是同一个指针,修改*_p也就修改了*p。
C) 使用返回值来传递动态内存
Char* GetMemory(int num)
{
Char *p = (char*)malloc(sizeof(char) * num);
Return p;
}
这里return返回的不是堆栈上的内存,所以不会在函数结束的时候被释放。
D)
Char* GetString(void)
{
Char p[] = “hello world”;
Return p;
}
数组p在堆栈中,函数结束后会被释放。
E)
Char* GetString(void)
{
Char *p = “hello world”;
Return p;
}
字符串常量位于静态存储区,在程序的整个生命周期都有效无论什么时候调用GetString返回的都只是一个只读的内存块的地址,可以把返回值改为const char*防止无意中的修改。
4,malloc free是C中的库函数,不会调用构造函数和析构函数,new delete是C++的运算符,会自动调用类的构造函数和析构函数。
第十七章 STL
1,六大组件:容器,存储分配器,迭代器,泛型算法,函数对象,适配器。