Effective C++是世界顶级C++大师Scott Meyers的成名之作,初版于1991年。在国际上,这本书所引起的反响之大,波及整个计算机技术出版领域,余音至今未绝。几乎在所有C++书籍的推荐名单上,这部专著都会位于前三名。
该篇为我在实习期间学习《Effective C++》的一些整理,会每一两天定期更新。
insert new line 代码的自动补全问题
Ctrl + . : 参数提示
Re-Indent 格式化代码
零、术语
第0章代码及注释:
#ifndef shuyu_h
#define shuyu_h
#endif /* shuyu_h */
typedef int NUM[100];//声明NUM为整数数组类型,可以包含100个元素
NUM n;//定义n为包含100个整数元素的数组,n就是数组名
typedef struct //在struct之前用了关键字typedef,表示是声明新类型名
{
int month;
int day;
int year;
} TIME; //TIME是新类型名,但不是新类型,也不是结构体变量名
#include <iostream>
class Shuyu{
public:
Shuyu(); //default 构造函数
// explicit可以抑制内置类型隐式转换,
// 所以在类的构造函数中,最好尽可能多用explicit关键字,防止不必要的隐式转换.
// 显示转换如 new Shuyu(10);
explicit Shuyu(int number);
//copy构造函数
Shuyu(const Shuyu ©);
/*
如果 Shuyu s1 = s2; (s2 已经被定义)
那么这个时候会有构造函数被调用,而不是赋值操作
如果是单纯的 s1 = s2 那么此时为赋值操作
*/
Shuyu& operator=(const Shuyu& copy);
/*
运算符重载的部分说明:
加const是因为:
①我们不希望在这个函数中对用来进行赋值的“原版”做任何修改。
②加上const,对于const的和非const的实参,函数就能接受;如果不加,就只能接受非const的实参。
用引用是因为:
这样可以避免在函数调用时对实参的一次拷贝,提高了效率。
copy构造函数是一个比较重要的函数,因为它定义了一个对象如何Pass By Value 即值传递
*/
};
explicit可以抑制内置类型隐式转换,
所以在类的构造函数中,最好尽可能多用explicit关键字,防止不必要的隐式转换.
1、typedef:为一种数据类型定义一个新名字。
在平台一上使用typedef long double REAL;,平台二如果不支持Long Double类型,就改为typedef float REAL,这样在别的用到REAL的地方就不需要修改了。
- 理解复制声明的技巧:从变量名看起,先往右,再往左
int (*func[5])(int *);
func 右边是一个[]运算符,说明func是具有5个元素的数组;func的左边有一个*,说明func的元素是指针(注意这里的 * 不是修饰func,而是修饰 func[5]的,原因是[]运算符优先级比 * 高,func先跟[]结合)。跳出这个括号,看右边,又遇到圆括号,说明func数组的元素是函数类型的指 针,它指向的函数具有int * 类型的形参,返回值类型为int。
- const char * p 的意思是p指向的目标空间的内容不可变化,
char * const p 的意思是指针p的值不可变,但它指向目标的值可变。
2、#define
#define 指令将标识符定义一个宏,程序在编译的时候会将相同的字符进行替换,也不作正确性检查,当替换列表中含有多个字符的时候,最好的将替换列表用圆括号括起来。宏定义不是说明或者语句,在行末尾不必添加分号;
- 在大规模的开发过程中,特别是跨平台和系统的软件里,define最重要的功能是条件编译。
#ifdef windows
...
#else
...
#endif
#ifdef debug
...
...
#endif
- define与Typedef的区别
#define是预处理指令,在编译预处理时进行简单的替换,不作正确性检查;
而typedef是在编译时处理的,它在自己的作用域内给一个已经存在的类型一个别名,
本书启示一:避免不明确(未定义)行为。
其它补充:
char name[] = " hello"
注意name数组大小为6,别忘了最后的null
一、联邦语言C++
C++是一种支持过程形式、面向对象形式、函数形式、泛型形式、元编程形式的语言。
-
a、C:C++是以C为基础的,block、预处理器、数组、指针等都来自于C
-
b 面向对象 :封装、继承、多态、封装
-
c Template
-d STL:STL是整个Template程序库
第一章练习代码:
.h文件:
/*
#define 不被视为语言的一部分 尽量不要用
#define 不能提供任何的封装性 即不存在 private #define 一类的东西
*/
//大写名称通常用于宏
const double Ratio = 1.65;
//由于常量通常在头文件内部(会被不同的源码调用) 因此需要将指针声明为const
const char* const authorName = "wushuohan";
#include <iostream>
class GamePlayer{
private:
// 头文件内常量声明
static const double Ratio;
//a=1是一个声明式定义
/*
通常C++需要一个定义式
但是如果这个该常量既是static又是整s数类型 可以忽略
*/
static const int a = 1;
std::string name;
std::string age;
public:
/*
const在*左边,被指物是常量
const在*右边,指针是常量
*/
void func1(const int * a);
void func2(int const* a );
//赋值说明 见函数的实现
GamePlayer(const std::string &name);
//构造函数的最佳写法
GamePlayer(const std::string &name, const std::string &age);
/*
尽量用local-static代替non-local static
构造顺序之Non-local static
函数内的static对象为local-static对象 其余均为non-local
static对象的析构函数会在main()方法结束时自动调用
C++对non-local static的构造顺序没有规定,
如有需要,可以把他们搬到自己的专属函数内,在函数内部是static,用函数返回一个reference;
函数内static对象会在函数被调用期间、首次遇上该对象定义时被初始化。
*/
int test(){
static int a = 6;
return a;
}
};
/*
取一个const的地址是合法的
但取enum的地址是非法的 指针指不到
单纯对于常量 尽量用 const enum 替换define 可以h降低对预处理器的需求
对于函数形式的宏,用inline函数来替换
*/
template<typename T>
inline void callWithMax(const T& a , const T& b){
f(a>b?a:b);
}
/*
内联函数是指那些定义在类体内的成员函数,即该函数的函数体放在类体内。
为什么inline能取代宏?
1、 inline 定义的类的内联函数,函数的代码被放入符号表中,在使用时直接进行替换,(像宏一样展开),没有了调用的开销,效率也很高。
2、 类的内联函数也是一个真正的函数,编译器在调用一个内联函数时,会首先检查它的参数的类型,保证调用正确。然后进行一系列的相关检查。这样就消除了它的隐患和局限性。
3、 inline 可以作为某个类的成员函数,当然就可以在其中使用所在类的保护成员及私有成员。
宏是由预处理器对宏进行替代,而内联函数是通过编译器控制来实现的
*/
class textBlock{
public:
//成员函数length()不该动对象内的任何一个Bit
std::size_t length() const;
//当const和Non-const有着相同的实现时,让non-const调用const
private:
//mutable定义的成员变量总是可能会更改,即使是在Const函数中
mutable bool lengthIsValid;
mutable std::size_t textLength;
};
内联函数是指那些定义在类体内的成员函数,即该函数的函数体放在类体内。
为什么inline能取代宏?
1、 inline 定义的类的内联函数,函数的代码被放入符号表中,在使用时直接进行替换,(像宏一样展开),没有了调用的开销,效率也很高。
2、 类的内联函数也是一个真正的函数,编译器在调用一个内联函数时,会首先检查它的参数的类型,保证调用正确。然后进行一系列的相关检查。这样就消除了它的隐患和局限性。
3、 inline 可以作为某个类的成员函数,当然就可以在其中使用所在类的保护成员及私有成员。
.cpp文件
#include "part1.h"
#include <iostream>
//实现文件常量定义
const double GamePlayer::Ratio = 1.5;
GamePlayer::GamePlayer(const std::string &name){
//注意:这里是赋值 不是对Name的初始化
//初始化应该在进入构造函数之前就发生了
(*this).name = name;
}
//构造函数的最佳写法 这样 构造函数不需要执行赋值操作
//先设新值再赋值太浪费了
/*注意:C++两个成员的初始化顺序也有差异 先base 再derived
这里先name,再age
*/
GamePlayer::GamePlayer(const std::string &name,
const std::string &age):name(name),age(age){}
std::size_t textBlock::length()const{
//具体略
return NULL;
}
二、构造/析构/赋值运算
2.1、构造
编译器会自动为一个类声明一个copy构造函数、一个copy assignment操作符、一个析构函数。如果没有声明任何构造函数,那么编译器会声明一个default 构造函数。
注意:C++ 不允许让reference改指向不同的对象
本章练习代码
#include <iostream>
template <class T>
class NamedObject{
public:
NamedObject(std::string &name,const T & value);
private:
//注意:C++ 不允许让reference改指向不同的对象
//因此如果需要用 object1 = object2 时 需要自己定义一个copy assignment操作符
std::string& nameValue;
//同时如果类内 内置了 const成员 编译器不会生成赋值函数 因为修改const是不合法的
const T objectValue;
//在private里声明 copy构造和copy assignment可以防止编译器自行创建
//而只声明不定义 是为了防止friend可以调用他们
//这样就阻止了编译器自动创建这些函数了
NamedObject(const NamedObject&);
NamedObject& operator=(const NamedObject&);//没有定义
};
2.2、析构
补充:C++的三种访问权限
三种访问权限
- public:可以被任意实体访问
- protected:只允许子类及本类的成员函数访问
- private:只允许本类的成员函数访问
三种继承方式:public、private、protected
1、public继承不改变基类成员的访问权限
2、private继承使得基类所有成员在子类中的访问权限变为private
3、protected继承将基类中public成员变为子类的protected成员,其它成员的访问 权限不变。
4、基类中的private成员不受继承方式的影响,子类永远无权访问。
带有多态性质的base-class必须声明一个virtual的析构函数,如果class的设计目的不是base-class,即不声明。
若 TimerKeeper * pw = new AtomicClock();
而此时 基类TimerKeeper的析构函数是non-virtual的
那么derived-class会经由based-class的析构函数销毁
那么derived-class的derived部分就不会被销毁
这种局部销毁会导致资源泄露
本章代码:
class TimerKeeper {
private:
public:
TimerKeeper();
//错误的写法 ---如果用作继承的话
// ~TimerKeeper();
virtual ~TimerKeeper();
};
/*
若 TimerKeeper *pw = new AtomicClock();
而此时 基类TimerKeeper的析构函数是non-virtual的
那么derived-class会经由based-class的析构函数销毁
那么derived-class的derived部分就不会被销毁
这种局部销毁会导致资源泄露
解决方案:析构函数前+virtual关键字
*/
class AtomicClock:public TimerKeeper{
public:
void close();
};
/*如果class不带virtual,通常说明它不被意图用作一个基类
这时不应将它的析构函数声明为virtual
因为会多些带一个virtual tavle pointer 来决定哪个方法被调用
会增加对象的体积
*/
class AbstractTest{
public:
//声明为抽象类 即该类不能创建对象
//最深层的derived-class的析构会先被调用,其次是每一个based-class的析构
//因此除了声明外,还需要对这个抽象类的析构函数创造一个定义。
virtual ~AbstractTest()=0;
};
//析构函数绝对不要吐出异常
//如果需要对某个异常的情况作出反应,那么可以在class内写一个普通函数执行该操作
class DBCon {
private:
AtomicClock ac;
bool closed;
public:
//有效的异常处理方法 定义自己的close 让异常 有处可寻
void close(){
ac.close();
closed = true;
}
~DBCon(){
if(!closed){ //如果客户端不关闭的g话
try {
ac.close();//可能会抛出异常
} catch (int e) {
std::abort();
/*
abort强迫结束程序
阻止异常从析构函数传播出去
当然可以不使用abort,在catch后吞下异常,让程序在遭遇错误后继续执行
*/
}
}}
};
-
析构函数绝对不要吐出异常
-
如果需要对某个异常的情况作出反应,那么可以在class内写一个普通函数执行该操作
-
条款9:在析构和构造时不要调用virtual函数,因为这类调用从不下降至derived-class这一层。
-
令赋值操作符返回一个reference to this *
2.3、自动赋值出现的问题
class BitMap {
};
//解决自我赋值的问题
//可能会出现指针指向一块已经被释放过的地址
class WidGet {
private:
BitMap *bp;
public:
WidGet& operator=(const WidGet &rhs){
/*
如果是同一个对象
delete bp删除的既是this的bp又是rhs的bp
因此需提前加一个证同测试
*/
// if(this == &rhs)return *this;
//更好的方法是不去理会 而创建一个新的拷贝
BitMap * bptemp = bp;
bp = new BitMap(*rhs.bp); //拷贝构造
delete bptemp;
return *this;
}
};
三、资源管理
3.1、以对象管理资源
如果在……发生了异常、return、goto等语句,那么delete将不会执行。
解决方法是将资源放进对象,对象的析构函数会自动释放这些资源。
本章代码1:
两种智能指针 以及 隐式转换Operator
//智能指针的原理是,接受一个申请好的内存地址,构造一个保存在栈上的智能指针对象,
//当程序退出栈的作用域范围后,由于栈上的变量自动被销毁,智能指针内部保存的内存也就被释放掉了
//智能指针会自动销毁它指向的对象,所以不能同时指向同一个对象
//为防止资源泄露 请使用智能指针对象 它们将在构造函数中获得资源并在析构函数中释放资源
class Investment{
public:
int daysHeld(const Investment*);
bool isTaxFree();
Investment* createInvestMent(){
/*在堆中分配*/
Investment *invest = new Investment();
return invest;
}
/*
如果在……发生了异常、return、goto等语句,那么delete将不会执行。
解决方法是将资源放进对象,对象的析构函数会自动释放这些资源。
*/
void func1(){
Investment *pInv = createInvestMent();
/*
调用指针……
*/
delete pInv;
}
//许多动态资源被分配后都用于单一的区块和函数中
//auto_ptr 智能指针
void funcAdvice(){
//createInvestment()的返回结果会作为智能指针的初值
std::auto_ptr<Investment> pInv(createInvestMent());
/*
像原来一样调用指针……
*/
}//会由智能指针的析构函数释放掉资源
void funcAdviceSecond(){
// shared_ptr也是一个智能指针,使用引用计数,每一个shared_ptr的拷贝都指向相同的内存。
// 每使用一次,内部的引用计数加1,每析构一次,内部的引用计数减1,减为0时,删除所指向的堆内存。
// 相比之前的auto_ptr多了拷贝构造 但不能打破环状引用
std::shared_ptr<Investment> pInv1(createInvestMent());
std::shared_ptr<Investment> pInv2(pInv1);
// ……………
}//pInv1 pInv2被销毁了
int test(){
//shared_ptr不会进行隐式转换 需用构造
std::shared_ptr<Investment> pInv(createInvestMent());
// 直接传原始指针
int days = daysHeld(pInv.get());
bool tax = pInv->isTaxFree();
bool tax1 =(*pInv).isTaxFree();
if(tax==tax1){
return days;
}
return 0;
}
// 隐式转换 operator + 返回结果 可以根据需要自动改变类型 但是不太安全
operator double()const{
// …………
return 0;
}
private:
};
本章代码2:
定义删除器
//每个人的地址有四行,每行是一个string
//在delete中也要使用delete[]
typedef std::string AddressLines[4];
/*
注意:
auto_ptr 与 shared_ptr都在其析构函数内做delete 而不是delete[]
*/
class Lock{
public:
int priority();
void processWidget(std::shared_ptr<int> pw,int priority);
void test1(){
// int 类型指针的赋值
int *a;
*a= 5;
int b = 5;
a = &b;
// 注意 构造函数里面为指针类型
//使用分离语句 务必以独立的new 语句将对象存入智能指针
//如果不这么做,将下面两句写成一句 一旦异常发生 可能会有难以察觉的内存泄漏
std::shared_ptr<int> pw(a);
processWidget(pw,priority());
}
void lock(int *);
static void unLock(int *);
//注意初始化 与 赋值 的区别 ptr先变成pm 然后再上锁
//shared_ptr的第二个参数是删除器,当引用计数为0时就会调用
explicit Lock(int *pm):sharedPt(pm,unLock){
lock(sharedPt.get());
}
private:
//智能指针的复制很有可能不合理
//方法1 把copy构造放在private里 不去定义
Lock(const Lock &);
Lock& operator=(const Lock &);
//方法二 使用shared_ptr 并且指定 删除器
std::shared_ptr<int> sharedPt;
};
四、设计与声明
注意:绝对不要返回一个reference 指向一个 local-栈 对象 或者返回reference指向 堆-allocated 对象
本章代码如下:
#include <iostream>
//类定义里面的成员变量和函数默认都是private型 类本身默认为public型
class Window {
public:
std::string name()const;
virtual void display()const;
/*
Window的copy构造会被调用 w会被初始化
当isOK返回时w会被销毁
*/
bool isOK(Window w);
// 可以用pass-by reference to const来避免这些构造和析构
bool isOkAdvice(const Window& w);
};
class WindowWithScrollBars:public Window {
public:
void display()const;
void printAndDisplay(const Window& w){
std::cout<<w.name();
w.display();
}
//对于内置类型 如(int),以及STL 通常用pass—By-value比较合适
};
/*
注意:绝对不要返回一个reference 指向一个 local-栈 对象
或者返回reference指向 堆-allocated 对象
*/
class Rational {
private:
int n,d;
/*
错误的写法
result是一个local对象,会在函数退出前被销毁
因此返回的reference指向旧的Rational
*/
// const Rational& operator *(const Rational & lhs,
// const Rational & rhs){
// Rational result(lhs.n * rhs.n, lhs.d * rhs.d);
// return result;
// }
//
/*
注意区别:
上面的Rational没有使用new 关键字
它在栈空间创建对象
函数退出时栈内存会被回收
而下面的Rational采用了new 只要是new就在堆空间分配
记住一个死规则 只要是new 就需要delete
*/
/*
更垃圾的写法
而这里new了以后,内存无人来释放delete
*/
// const Rational& operator *(const Rational & lhs,
// const Rational & rhs){
// Rational *result = new Rational(lhs.n * rhs.n, lhs.d * rhs.d);
// return result;
// }
//友元函数可以在类内的任何地方声明 不受域的影响
friend const Rational operator * (const Rational & lhs,
const Rational & rhs);
public:
Rational(int a= 0, int b =0);
};
从封装性来看,只有两种封装性,private和其它。
如果有一个public或者protected的成员变量被更改,会有不可预知的大量代码被更改。
切记将成员变量声明为private,这可以提供客户访问的一致性。
如果某些东西被封装,它们便不再可见。它可以使我们改变事物而只影响有限客户。愈多的函数可以访问数据,它的封装性越差
偏特化:
函数模板没有偏特化,因为有函数重载的概念,C++根据参数的类型来判断重载哪一个函数,如果还进行偏特化,这就与重载相冲突。但是,我们可一个对模板进行重载,从而实现偏特化。
模板的实例化类型确定是在编译期间
练习代码:
/*
模板的实例化类型确定是在编译期间
全特化一般用于处理有特殊要求的类或者函数,此时的泛型模板无法处理这种情况。
模板为什么要特化,因为编译器认为,
对于特定的类型,如果你对某一功能有更好地实现,那么就该听你的。
C++只允许对class template偏特化
对function-template的偏特化是不合法的
模板实例化只会实例化用到的部分,没有用到的部分将不会被实例化
*/
//原始的模版
template <typename T,typename T1>
class Test{
public:
bool compare(T& a,T &b){
return (a<b)?true:false;
}
};
//全特化 可以看作是一种重载
template <>//参数都指定了 所以参数列表为空
class Test<int,char> {
public:
bool compare(int &a,char &b){
return false;
}
};
//偏特化 只指定部分的参数类型
template <typename T>
class Test<int,T>{
public:
bool compare(int & a,T& b){
return false;
}
};
//注意 函数没有偏特化 只有全特化
//函数模板没有偏特化,因为有函数重载的概念,C++根据参数的类型来判断重载哪一个函数,如果还进行偏特化,这就与重载相冲突。但是,我们可一个对模板进行重载,从而实现偏特化。
网友评论