Tag Archives: C++

C++ Primer 笔记(5)

Chapter 14

这章主要讲操作符重载。

操作符重载不能用于操作数都是内置类型的时候。比如,我们不能重载operator+(int,int)。操作符的优先级、结合性、操作数个数不能改变。重载&&,||和逗号后,不再保证求值顺序和短路运算,最好不要重载这几个。

大部分重载操作符可以作为普通函数或类的成员函数,在第二种情况下,参数列表里会少一个,因为省略了隐含的this。一般将算术和关系操作符定义非成员函数,而将赋值操作符定义为成员(如上章介绍)。如果作为普通函数,因为往往要访问私有成员,在类定义里往往要将该函数设为友元。操作符也可以用类似函数调用的形式访问,像这样 loli = operator+(loli, loli2).

赋值(=)、下标([])、函数调用(())、成员访问(->)这几种操作符必须定义为类成员。+=, ++之类,最好定义为成员。对称的运算,如+, ==,往往定义成普通函数。

重载IO操作符应该声明为普通函数,不然的话类就只能作为左操作数,就会变成这样的诡异形式 loli<<cout,明显不符合习惯。而我们又不能为iostream类添加成员,所以只能定义成普通函数了,然后把它作为我们的类的友元。

重载加法时应该返回一个新的对象而不是引用(所以a+b是右值),而+=一般返回的是对左操作符的引用。(这种情况下用+=效率高些?)

某些算法(例如find())以及关联容器需要我们定义<或是==,要注意<的定义满足以前说的那几个条件。

下标运算符可能作为左值,故应返回引用。但对const对象应返回const引用,所以应该写const与非const两个版本的重载函数,如chapter 12所讲。

重载自增和自减操作符应该遵循惯例,前置返回引用,应该这样写:Loli& operator++(),后置返回变化之前的,应该这样写:Loli operator++(int),最后那个int是为了与前置区分,在函数体内无视它。

定义了函数调用操作符的类,又被称作函数对象,因为这种类的对象看上去表现得像一个函数。这种东西最常用在STL里,比如我们自己定义一个结构体的比较函数对象,优先按照年龄,然后按照姓名排序:

struct Lolicmp {
    bool operator()(const Loli &a, const Loli &b) {
        if (a.age != b.age) return a.age < b.age;
        return a.name < b.name;
    }
};

std::set<Loli, Lolicmp> loli_set;
std::vector<Loli> loli_arr;
sort(loli_arr.begin(), loli_arr.end(), Lolicmp());

标准库也定义了若干函数对象,比如我们最常用的greater<T>,所以把int数组从大到小排序就是sort(a, a+N, greater<int>())

Chapter 15

本章介绍OOP的核心:类继承和动态绑定。继承就是指类之间的层次关系,一个类是另一个类的派生类之类。动态绑定是指在运行时才决定是调用基类的成员函数还是派生类的成员函数,这就产生了所谓多态。C++里实现要想实现动态绑定需要用对象的引用或指针,看下面例子:

class Loli {
public:
    virtual void speak() { cout<<"oniisan~"<<endl; }
    virtual ~Loli() { }
    Loli() : age(0) { }
protected:
    int age;
};

class TsundereLoli : public Loli {
public:
    void speak() { cout<<"ulusai! I am "<<age<<" years old."<<endl; }
};

void func(Loli &t) { t.speak(); }

int main() {
    Loli a;
    TsundereLoli b;
    func(a);
    func(b);
    return 0;
}

这个程序有很多地方需要解释,下面会慢慢说。

执行上面程序会发现,两次调用func()实际上调用了不同的函数,尽管func里写的是接受基类的引用(在书里这个引用或指针的类型叫做静态类型),但传进去的对象会“知道”自己实际的类型(书里叫动态类型)。要实现这个功能,一定要把基类对象的相应成员函数声明为virtual,只有虚函数才会执行动态绑定,普通成员函数的执行是在编译期就确定的。如果上面例子中的speak()不是虚函数的话,因为func接受的是基类Loli的引用,故一定调用基类的speak()。注意,一旦基类里声明某函数为虚函数,它就一直是虚函数,在派生类的实现里无需再带virtual关键字(是不是带上更清晰些?)

基类与派生类之间的转换

如上所述,存在派生类引用(或派生类指针)到基类引用(或基类指针)的自动转换,这就是实现多态的关键。反之的自动转换则不存在,例如如果把上面func()函数改成接受TsundereLoli的引用,则编译不过,因为func(a)这句要求一个基类引用到派生类引用的转换,这是不能自动进行的。

上面说的是指针或引用的转换,对象的转换则完全不同,如果把上面的func函数改成 void func(Loli t) {…},此时它自始至终都是接受一个Loli类型的对象,不存在动态绑定,如果把TsundereLoli对象传递过去,则TsundereLoli会被强行“切割”(slice down)成一个Loli对象,然后调用Loli::speak(),TsundereLoli对象的任何不属于Loli子对象的数据成员和函数成员将被丢弃。

下面来详细分析一下原因:

当用派生类对象对基类对象进行初始化或赋值时,有两种可能的情况发生:第一种情况很罕见,就是在基类里有一个以派生类为参数的复制构造函数和赋值运算符,像下面这样子:

class TsendereLoli;
class Loli {
public:
    Loli(const TsendereLoli &) { ... }
    Loli& operater=(const TsendereLoli &) { ... }
};

这时我们可以自己定义从派生类对象转换到基类对象时做什么。不过这种情况显然是非常罕见的,理论上基类的作者不应该知道具体是谁继承了自己,他只应该对自己以及父类负责。第二种情况是常见的,也就是基类里只有以自己的引用为参数的构造函数和赋值运算符(如果不写的话默认产生的也是这种):

class Loli {
public:
    Loli(const Loli &) { ... }
    Loli& operater=(const Loli &) { ... }
};

这时如果我们把一个派生类作为参数传进构造函数或赋值过来,这时会发生的事情是:首先将基类类型的引用(Loli&)绑定到一个派生类的对象,然后将这个引用作为实参传递给复制构造函数或赋值符,因为是基类的引用且非虚函数,函数内部只能使用到派生类里的Loli子对象部分,如果调用函数的话也只能调用基类的函数,通常情况下函数内部的实现是把基类的成员一一复制过来(不写的话默认也是这样的),当函数结束后,就得到了一个真正是基类的对象,不存在任何派生类的成分,这时我们就说派生类被slice down了。

(15章未结束,待续)

C++ Primer 笔记(4)

Chapter 12 (cond)

关于构造函数

所谓构造函数初始化式(Constructor Initializer)是指在构造函数参数列表后面冒号里的那一列东西,这是一个很多熟练程序员都可能不太了解的地方,大家可能更习惯在构造函数的花括号里写东西。但是实际上只有在冒号后面的那些才是真正的初始化,在花括号执行的只是普通的赋值语句,明确地区分这两者看上去有点无聊,但某些情况下是必须要考虑的。一是效率上的原因,不管怎么写,在创建对象时都要先对每个成员进行初始化,然后再执行花括号里的普通计算。如果写在花括号里,实际上是先初始化一次,再赋值一次覆盖掉初始化的内容,这样就造成了浪费,如果成员是比较大的对象,效率上会有影响。二是某些对象是不允许赋值的,比如引用和const成员,这时只能在初始化式里初始化,不能在花括号里赋值。总之,尽量写在初始化式里比较好。

初始化的次序与成员声明的次序一致,与初始化式里的次序无关。尽量不要让初始化的值依赖其他成员变量,以免造成混乱。

如果初始化式里没有指明某个成员变量,则按照默认方式初始化:对于类类型成员调用默认构造函数,对于内置类型成员,如果是全局对象则置0,局部对象则为未定义值。

如果对类A声明了任何一个构造函数,则编译器不会再自动合成默认构造函数。这种情况下会带来诸多不便,比如如果某个类B以类A作为一个成员,则类B也不再有默认构造函数;不能声明A的静态数组;包含A的容器也不能只用一个表示大小的值来初始化,等等。所以通常情况下最好要有个默认构造函数。

如果类有接受一个参数的构造函数,这时就隐含了一种类型转换的可能,见下面代码:

class Loli {
public:
    Loli(int n) : age(n) {}
    int get_age() {return age;}
private:
    int age;
};

void print_loli_age(Loli loli) {
    cout<<loli.get_age();
}

虽然print_loli_age要接受一个Loli类型的参数,但我们可以直接调用print_loli_age(15)。因为Loli有一个接受一个int参数的构造函数,这样就把15自动转化生成了一个Loli类型。有的时候这样的隐式转换不是我们想要的,就需要在构造函数前面加一个explicit来禁止这种转化,此时如果想转化的必须显式调用:print_loli_age(Loli(15))

关于static成员

static成员独立于类的任何实例对象而存在。static成员函数没有this指针,也不能访问非static成员。static数据成员必须在类的定义体外定义恰好一次,在定义同时初始化,通常把它和类的成员函数的实现放在同一个源文件里。

Chapter 13

本章主要是三部分内容:复制构造函数、赋值运算符和析构函数

复制构造函数是一种特殊构造函数,具有单个形参,该形参(常用 const 修饰)是对该类类型的引用。当定义一个新对象并用一个同类型的对象对它进行初始化时,将显式使用复制构造函数。当将该类型的对象传递给函数或函数返回该类型的对象时,将隐式使用复制构造函数。

在初始化的时候,如果是直接在圆括号里写参数,例如 string s(10, ‘a’),则调用相应的构造函数,如果出现了赋值号(不知道这么说严谨不),例如 string s = “aaaa”,则先生成一个临时对象,再调用复制构造函数复制过去。

当对象作为非引用的参数传递给函数的时候,以及从函数里返回一个非引用的对象的时候,会调用复制构造函数生成一个副本。另外如果是定义了类似vector<T> v(n)之类的东西的话,会先用默认构造函数生成一个临时对象,然后用复制构造函数复制n份。

如果没有显式声明复制构造函数,编译器会自动生成一个。多数情况下这就够用了,但是有些情况下还是不行的,比如对象里面有个指针或引用而我们想实现一个deep copy的功能,或者对象里有一个不可复制的成员,这时候就要自己定制了。定制的复制构造函数和普通的构造函数没什么大区别,接受一个相同类型的引用(通常是const)作参数,推荐用初始化式进行初始化,然后在花括号里干其他的事。

如果我们想禁止对象被复制,应该写一个private的复制构造函数(不能不写,不写会自动生成),但这样的话自己的成员函数和友元还是可以调用它,进一步的方法是只声明而不实现(好强-_-)。这样的话,如果是外部调用,会编译错误,如果是自己的成员函数或友元调用,会链接错误。

重载赋值运算符时,代码的写法看上去就像把operater=当作类的一个成员函数一样,它接受一个相同类型的const引用作为参数,返回值是同一类类型的引用。注意处理好赋值给自己的情况,这种情况通常要特判一下。

如果是new出来的对象,只有在delete时才会被删除(此时调用析构函数)。如果忘了删除就会导致所谓内存泄漏。内存泄漏往往只有在程序长时间运行后内存消耗严重时才表现出来,比较难于发现。

关于智能指针:所谓智能指针是指保存了一个“引用计数”的指针,它可以避免当多个指针指向同一对象时,通过其中某个指针把对象删除了,但其他的指针并不知道,继续访问就会导致错误。通过保存一个引用计数,只有当指向这个对象的指针数为0时,才真正把对象删除。在这种情况下需要自己实现一个指针类,把原生指针包装一下,通过重写该指针类的构造、复制、析构函数实现引用计数的管理。在13章的最后给了一个很好的例子。(ps. 在boost里有标准的智能指针实现了?)

C++ Primer 笔记(3)

后面的内容我比较不熟,所以写得详细些。

第12章到第14章是本书的第三部分,是关于“类和数据抽象”。把数据和对数据的操作结合在一起,形成所谓的抽象数据类型(ADT)。只把操作暴露给类的使用者,把具体的实现隐藏起来,类的使用者无需关心,这就体现了传说中的“封装”。以这种方式写程序被称为“基于对象”(Object-Based),它并不是完全的“面向对象”(Object-Oriented),没有涉及类的继承、多态、虚函数、动态绑定等等复杂的机制(这是本书第四部分的内容)。很多国产书上往往对此混淆不清,让读者以为在结构体里加个方法就是面向对象了,对于真正面向对象的精髓一笔带过,读者看完整本书都不知道所谓多态到底是怎么回事。

完全彻底的“面向对象”不一定就是最好的,很多情况下用基于对象的方法才是最合适的,比如STL可以说就没有面向对象的内容。对于小程序来说甚至面向过程的方法就已经足够,没有必要搞得过于复杂。

Chapter 12

在类内部定义的成员函数默认为inline,在类外部定义的成员函数要用class_name::member_func这样的方式指明其所属的类。非static的成员函数有一个隐含的参数this,它是一个指向调用该函数的对象的指针。如果把成员函数声明为const,相当于这个this指针是const指针,也就是不能修改对象的内容了。另外,声明时这个const必须在声明和定义时都出现,否则编译不过(原因很有趣,后面会解释)

class和struct关键字的区别只有一点,class里成员默认是private,struct默认是public。

在类的内部可以用typedef定义类型别名,比如

  class Loli {
  public:
    typedef unsigned short age_type;
  private:
    age_type age;
  };

这样就有了一个Loli::age_type的类型了,类的实现会保证它能处理任何年龄的loli,我们无需关心它具体是个什么类型。标准库里的string::size_type也是同样的道理。

类的定义在遇到右花括号时结束,这时编译器就知道了全部的类的成员,以及存储一个对象所需的空间(成员函数的实现可能未知)。类的定义通常写在头文件中,然后被include到各个源文件中。一个源文件中同一个类的定义只能出现一次(这一点可以用#ifdef机制保证),多个源文件中可以多次出现同一个类的定义,但它们必须完全相同。

也可以先声明而不定义一个类,像这样 class Loli; 这样的语句被称为前向声明,这时得到一个“不完全”的类型,因为编译器只知道一个名字,此时只能以有限的方式使用它。使用方式可以是定义指向该类型的对象的指针或引用,或者声明(而不是定义)一个以该类型对象作为参数或返回值的函数。但是不能定义该类型的对象,也不能定义上面提到的那种函数。总之就是不能有涉及类的内部成员的东西。有时候我们需要互相依赖的类(A里有B的引用,B里有A的引用),这时就需要这种前向声明。

this指针一般不需要显式地指明,但是在我们需要返回一个对象自身的引用时,需要写成 return *this; 这个样子。见下例:

class Loli {
public:
    typedef unsigned int age_type;
    Loli &set_age(age_type k) {age = k; return *this;}
    Loli &grow_up() {++age; return *this;}
    Loli &push() {
        cout<<"You pushed a loli who is "<<age
            <<" years old."<<endl;
    	return *this;
    }
    Loli() : age(0) { }
private:
    age_type age;
};

这样我们就可以方便地按如下方式连成一串调用了:

Loli a;
a.set_age(14).grow_up().push();

进一步地,我们发现push()这个操作不改变loli的状态(其实是改变的吧XD),所以可以希望将其声明为const。另外一个原因是,如果我们有一个const的loli对象,也希望能进行这个push()调用。但是我们会发现如果只把成员函数后面加个const是编译不过的,原因在于返回值还是非const的,这样在return的时候会试图把const的*this转换成非const,而这是不允许的,所以应该写成:

class Loli {
public:
  ...
  const Loli &push() const { ... }
}

但是这样我们又发现了新问题,下面的语句

a.set_age(14).grow_up().push().grow_up();

竟然编译不过。其原因在于push()返回了const对象引用,这个引用不允许再访问非const的grow_up()了。解决方法是写两个不同版本的push(),分别支持const和非const,如下:

class Loli {
public:
    typedef unsigned int age_type;
    Loli &set_age(age_type k) {age = k; return *this;}
    Loli &grow_up() {++age; return *this;}
    Loli &push() {
        cout<<"You pushed a loli who is "<<age
            <<" years old."<<endl;
    	return *this;
    }
    const Loli &push() const {
        cout<<"You pushed a loli who is "<<age
            <<" years old."<<endl;
    	return *this;
    }
    Loli() : age(0) { }
private:
    age_type age;
};

可见“const与否”也是作为了“函数签名”的一部分,是可以重载的。现在这种写法就可以针对Loli对象到底是不是const而调用不同的函数。这也说明了前面的问题:为什么const成员函数里那个const字样要在声明和定义里都出现,否则他们就不是指的同一个函数了。另外,上面的写法出现了代码重复,最好把重复的部分提取出来放到一个private成员函数里。

如果希望一个成员变量无论如何是可变的(即使在const对象里),应该把其声明为mutable。

C++ Primer 笔记(2)

Chapter 7

C++里的参数传递始终是“按值传递”(或者说至少可以都从这个角度理解)所以如果是比较大的对象作为参数的话,复制这个对象的花费可能很大(还有一种情况是对象不可复制),这时候比较好的做法是传递指针或引用(在C里是没有引用的,C++里应该是更推荐后者?)如果在函数内部不会修改这个对象的话,更好的方法是将参数声明为const引用。

这里有一个比较容易忽略的问题,如果参数是非const引用,调用时传递一个右值或者需要类型转换的对象都是不可以的,比如下面的语句是不合法的

  int incr(int &loli) {return ++loli;}
  int main() {
    incr(13); //invalid
    int loli = 13;
    incr(loli + 2); //invalid
    short loli2 = 4;
    incr(loli2); //invalid
    const int loli3 = 14;
    incr(loli3); //invalid
  }

可见非const引用的参数是很不灵活的,如果只是为了节省复制参数的时间,通常总应该写成const引用。

以数组做为非引用参数的时候,注意数组名字会自动当作指针处理,在函数形参列表处指明数组大小是没有作用的。可以试试这个

  int func(int loli[10]) { cout<<sizeof(loli); }

千万不要返回指向局部变量的引用或指针。返回的引用是一个左值,因此也可以被赋值,虽然看上去 func() = 1 这样的东西很奇怪。如果不想被赋值,可以明确地返回const引用。

static 局部对象确保不迟于在程序执行流程第一次经过该对象的定义语句时进行初始化。这种对象一旦被创建,在程序结束前都不会撤销。

定义在类内的函数被隐式地当作inline的。const成员函数不能修改对象里的成员,相当于那个this指针是const的。

类对象的成员的初始化应该在构造函数里,而不是声明这个成员的时候。构造函数可以有一个“初始化列表”(initializer list),在构造函数的形参列表之后,花括号之前。

关于函数重载:如果局部地声明一个函数,则该函数将屏蔽而不是重载在外层作用域中声明的同名函数。由此,每一个版本的重载函数都应在同一个作用域中声明。一般来说不应该局部地声明函数。

编译器会按照某种度量自动查找最“接近”的一个同名函数声明进行调用,如果同时有多个同名函数在这种度量下“距离”相同,就会编译错误。这个距离具体是怎么算的,我懒得仔细分析了,汗。

最后是关于函数指针,我们在单独写出一个函数名字的时候(没有后面的圆括号),就指的是指向这个函数的指针。假设pf是一个函数指针,则下面两种方法都可以调用pf指向的函数:

pf();
(*pf)();

严格上讲似乎应该只是后者才对,某些老编译器可能对前者报错。

Chapter 8. 没兴趣,暂略 – –

Chapter 9.

第9-11章是详细讲STL的内容。第九章是顺序容器:vector, list, deque,以及几种适配器stack, queue, priority_queue

顺序容器可以在初始化时指定容器的大小,如果不指定的话默认为空容器。如果指定了大小而没有指定初始化的值,会自动进行所谓值初始化(value-initialize),对于基本类型赋0值,对于有默认构造函数的类会调用默认构造函数,如果没有默认构造函数但有其他的构造函数,就会编译错误。如果什么都没有,自动对每个成员再进行值初始化。

当对vector和deque插入和删除元素时,注意原来的迭代器会失效。

对容器赋值时,会把左操作数的容器清空,然后依次把右面的数insert过去。而swap操作可以保证在常数时间内完成交换,并且原来的迭代器仍然有效(可以想象成交换了一下两个指针)。

vector的capacity()表示预留的空间大小(注意与size()的区别),当用完时通常会采用加倍的策略。

本质上,vector是数组,list是链表,这都没啥好说的。deque是一个比较nb的结构(记得好像是好多个数组串起来?),它支持随机访问,支持常数时间在两头增删,在两头增删时中间的迭代器都仍然有效,不过在中间增删还是需要线性时间的,也会导致迭代器失效。

Chapter 10

map里的键是使用<操作符来进行比较,如果是自定义类型,应该对其进行重载。比较函数应该满足如下三个条件:一个键与其自身比较,一定返回false;任意两个键不能出现相互小于的情况;如果a小于b,b小于c,则a必须小于c。对于任意两个键x,y,如果不存在x小于y和y小于x,则认为他们相等,无法区分这两个键。

map::value_type是一个pair<const map::key_type, map::mapped_type>,注意第一个元素是const的。

使用下标访问一个map中不存在的key,会导致插入一个新元素并初始其值为0。有时候这样会比较浪费,可以根据具体需求考虑用count(), find(), insert()替换。

如果multiset, multimap的erase方法接受参数是迭代器,它会删除迭代器指向的那个元素,如果参数是一个键值,会把所有相同键值的元素都删除。

Chapter 11

accumulate函数作用是把容器内的一段数累加,注意是以第三个参数为初始值,然后把其依次与每个数作加法(故要求容器里的数与初始值类型一致或可以转化成初始值的类型),返回值也是第三个参数的类型。下面的代码是意图把一系列字符串连接起来,但是是编译不过的:

vector<string> vs;
accumulate(vs.begin(), vs.end(), "");

因为第三个参数是const char*类型,string类型不能转化为这个类型,应该把第三个参数写成string(“”)。另外一个更tricky的错误是:

vector<double> vd;
accumulate(vd.begin(), vd.end(), 0);

迭代器有五种:输入迭代器、输出迭代器、前向迭代器、双向迭代器、随机访问迭代器

因为不支持随机访问,list提供了与容器相关联的几个算法,比如list.sort,这个和algorithm里的sort是不一样的(algorithm里的sort是所谓introsort,list的sort估计只能用归并排序吧?)。list容器里的merge和splice会破坏它们的实参,这个和通用的merge函数不一样。

C++ Primer 笔记(1)

还是应该认真地学一点C++啊……以下记录我在看书的过程中认为值得记下的东西,请大牛指教。前面几章因为内容比较熟悉了,记得就比较简略。

Chapter 1

在这句话 cout<<“hello”<<endl; 里,<<是一个操作符,它左边的操作数是一个ostream对象(在本例中为std::cout),运算的结果是左边的值,也就是还是cout,上面那句话等价于 (cout<<“hello”)<<endl;

把std::endl写入ostream时,会输出一个换行,并刷新缓冲区。在用printf或是cout打log进行调试时,应该意识到缓冲区的存在,如果没有及时刷新的话,真正显示到屏幕上的时刻未必就是输出语句执行的时刻。

Chapter 2

表达式可以分为lvalue和rvalue两种,lvalue只指可以放在赋值符号左边的表达式,lvalue可以使用的范围比rvalue更广。下面语句是不合法的 (a+b)=12,因为(a+b)不是lvalue。

C++有两种初始化变量的方式:直接初始化和复制初始化。两种方式分别如下所示

int loli(14);
int loli2 = 15;

初始化不是赋值,虽然第二句看上去是赋值,但它们是不同的。在内置类型(比如int, double, char之类)上效果上虽然没什么区别,但如果是类类型,可能就会有很大的不同(以后详述)。

引用(reference)实际上就是变量的一个别名,它只能在初始化的时候绑定到另一个变量上,并且以后不能更改。类似这个样子 int loli2 = &loli; int &loli2 = loli。通过这个引用修改变量,会导致访问原来的变量时也是修改后的值。

const引用是指通过这个引用只能读取而不能修改其绑定到的对象。如果一个对象是const的,那么不能把通常的引用绑定上去(因为这样在语义上就可以通过这个引用对本来是const的对象进行修改了),只能绑定const引用。另外,因为不可修改,const引用也可以绑定到右值上,比如const int &loli = 14是合法的,而 int &loli2 = 15不合法。

关于变量/函数的定义与声明,以及头文件的相关内容,我以前写过两篇Blog()。

Chapter 3

为了避免名字冲突,C++里引入了命名空间(namespace)的概念,通常来讲要使用一个名字,必须指明它所在的命名空间。比如标准库里的东西都在std这个里面,在使用的时候应该std::cin, std::vector<std::string>等等。但是我们往往为了省事,直接在程序前面用using语句,比如我用了 using lolispace::loliname 这样的语句,那么以后用到loliname这个名字时,就不需要写成lolispace::loliname这样了。但是如果我们要用到lolispace里另外一个名字叫做anotherloli的,就还需要在前面加一条using lolispace::anotherloli 。更偷懒的办法是直接用一条using namespace lolispace完全包括。因为标准库里的内容都是在std里,这也就是我们常常写一个using namespace std在最前面的原因。不过如果你还用到了别的命名空间的话,这种粗暴写法是很有可能导致冲突的。

另外,注意不要在头文件里用using语句,在头文件里一定要用完整的名字,因为你不知道你的头文件会被谁include,乱写的话就污染了人家的命名空间。

记录一个string的长度的安全方法是用string::size_type类型,而不是直接用int(我觉得这个不大可能真的会出问题,不过编译器往往会提出警告)。还有STL里的各种容器的大小也一样。

在C++里如果要使用原来C语言里的函数,推荐使用<cname>系列的头文件。比如cstdio, cstring, cmath, cctype等等。一个好处是这里面定义的名字都是在std命名空间里,这就与C++的库保持了一致。

vector<int>::const_iterator 和 const vector<int>::iterator 是完全不一样的。前者是一个“只读”迭代器,它指向的位置可变,但是不能通过它修改内容。后者是一个不能动的可读可写的迭代器,只能在初始化的时候指定位置。后者基本上不可能出现在实际情况中。

Chapter 4

在定义数组时,数组的维数必须是一个const对象,且在编译期就能确定下它的值。所以下面的写法是错误的(虽然在大多数编译器上可以通过):

int N = 10;
int arr[N];

在当C++课助教时发现有无数的孩子这么写。另外插一句,C99的标准允许这样写,所以目前来说这是正确的C程序,不正确的C++程序。

局部数组是不会进行默认初始化的,除非你显式地指定初值。比如 int local_array[5] = {1,2,3,4,5}。如果后面花括号里的值比数组的维数小,则后面的部分以默认方式初始化(int置0,string置空串等等),所以初始化一个全零的局部数组的简便写法是 int local_array[MAX] = {0}。

直接用字符串字面值(literal)初始化一个字符数组时,注意最后有一个。如果这样写 char str[] = “loli”; 那么str的维数是5而不是4.

const int *p表示不能通过p修改p指向的内容,但p本身的值可变(可以指向其它地方),而 int *const p表示p的值不可变,它指向一个int,并且可以通过p修改int的值。

用new动态分配了数组后,一定要用delete[]删除,不要漏了那个中括号。

Chapter 5

优先使用前置自增(减)操作符,比如++x。因为后置操作符(x++)必须先保存操作数原来的值以便在表达式结束时返回原来的值。如果操作数是基本类型,影响微乎其微。但如果操作数是一个重载了自增操作符的复杂的类类型对象,保存中间结果这一步的花费可能就很可观了。

C++语言里,&&, ||, ?: 以及逗号操作符明确规定了求值顺序。对于&&,先计算左侧的值,如果为假则不再计算右侧的值。对于||,如果左侧为值则不再计算右侧。这两个被称为所谓“短路(short-circuit)求值”。对于三元操作符?:,会根据问号左侧的结果选择某一个分支求值,另一分支不再计算。对于逗号,规定从左到右依次求值。对于其他的操作符,两侧操作数的求值顺序标准里没有规定,取决于具体的编译器实现,比如要计算 f1() * f2(),我们不知道这两个函数到底哪个先被调用。所以你现在可以痛骂某些脑残笔试题了。

C++里的强制转换有四种,static_cast, dynamic_cast, const_cast, reinterpret_cast。dynamic_cast涉及多态等内容,留待后叙。const_cast是负责强制去除对象的const属性。static_cast负责通常的强制转化,比如把double截断成int(如果没有显示转化的话编译器通常会给warning),把一个void*转换成一个有类型的指针之类。reinterpret_cast是一个更加暴力的逐位转化,比如下面代码

int *ip;
char *pc = reinterpret_cast<char*>(ip);

把一个整数强行解释成一个字符数组。一定要慎用强制转换,通常来说dynamic_cast和static_cast有少数时候确实要用;如果发现非要用const_cast不可,往往是你的设计出了问题;reinterpret_cast不要用。当然C语言里的强制转换是最剽悍的,把这些全部通吃,非常霸气。XD