在我们的学习中,对于基本的C/C++知识的掌握可以说问题不大,但是如果想进一步的学习c++的相关知识,这本书确实很经典,言简意赅的介绍了C++很多的特性,给我们对C++的理解越来越深刻,所以十分推荐C++的爱好者好好看一下。这几篇文章是自己感觉重要的地方,希望对大家也会有所帮助。
1、STL 并不仅仅是一个库,它更是一种优秀的思想以及一套约定。STL包含三大组件:容器、算法和迭代器。容器用于容纳和组织元素;算法执行操作;迭代器则用于访问容器中的元素。STL的优秀思想体现在:容器与在容器上执行的算法之间无需彼此了解,这种戏法是通过迭代器实现的。
迭代器类似于指针,我们可以像使用指针一样来使用迭代器,通过迭代器我们可以访问存放在容器中的数据;
容器是对数据结构的一种抽象,一类模板的方式实现而成;
算法是对函数的一种抽象,采用函数模板实现;
再详细点来说就是,通过定义一个容器我们可以向其中存放我们的数据(基本数据或者用户定义的数据类型),这个容器自身会带有两个迭代器(头尾各一个),然后通过自己定义的数据处理函数或者容器自带的算法,通过利用迭代器来访问容器数据成员,进而实现我们的目的。
详细的内容大家可以参看:http://www.cplusplus.com/reference/stl/(这个网站也是作者很喜欢的网站,相当于c++完全手册了)
在应用中我们是无法直观的看到迭代器这个东西,他是在幕后的工作者就像一个数组元素a[5]一样,没有出现指针,但却得到了a[5]的值。举个例子:
// constructing vectors
#include <iostream>
#include <vector>
using namespace std;
int main ()
{
unsigned int i;
// constructors used in the same order as described above:
vector<int> first; // empty vector of ints
vector<int> second (4,100); // four ints with value 100
vector<int> third (second.begin(),second.end()); // iterating through second
vector<int> fourth (third); // a copy of third
// the iterator constructor can also be used to construct from arrays:
int myints[] = {16,2,77,29};
vector<int> fifth (myints, myints + sizeof(myints) / sizeof(int) );
cout << "The contents of fifth are:";
for (i=0; i < fifth.size(); i++)
cout << " " << fifth[i];
cout << endl;
return 0;
}
2、引用和指针的三大区别(相信有很多人没有注意或不知道这个知识点):
不存在空引用,但有指向为空的指针
所有引用都要初始化,但指针不用
一个引用永远指向用来对它初始化的那个对象,除了常量指针以外。
3、数组形参:数组形参是一个讨厌的家伙,和他亲近时要小心
数组形参是一个容易出问题的地方。在C++中根本不存在所谓的数组形参,因为数组在传入时,实质上只传入指向其首元素的指针。
void average (int ary[12]) //实际上只传入ary[0]的地址,因此数组的长度12也会因此被舍弃,无法传递到average函数当中去。
{…………}
这种从数组到指针的自动转换被赋予了一个迷人的技术术语:退化。即数组退化成指向其首元素的指针。另外同样的事情也发生在函数上。一个函数形参回退化成一个函数指针,不过,
和数组在退化时会丢失边界不同,一个退化的函数具有良好的感知能力,可以保持其参数类型和返回类型。
由于在数组形参中数组边界被忽略了,因此通常在声明时最好将其省略。
声明一个数组作为参数的形式:
void average(int ary[])
如果数组边界很重要,希望函数只接受含有特定数量元素的数组,也用一个引用形参:
void average(int (&ary)[12]) //现在函数只能接受一个长度为12的数组
另外还有更传统的方法是将数组大小明确的传入函数:
void average(int ary[],int size) //size 是数组ary的长度
要注意不可以用指向数组的指针来初始化 int (&)[]
void averagefun(int (&ary)[12])
int *anArray=new int[arysize];
//……
average(anArray); //错误不可以使用int *初始化int (&) [n]
averagefun(anArray,arysize); //没问题
出于这些原因,经常采用某种标准容器(通常是vector或string)来代替对数组的大多数用法,并且优先考虑使用标准容器。
如果是多维数组因该用下面的的方式声明:
void process(int (*ary) [20]) //一个指针,指向一个具有20个int元素数组
或
Void process(int ary[][20]) //注意第二个参数20并没有退化。
多维数组是数组的数组。
4.常量指针与指向向量的指针
常量指针:T * const p;
指向常量的指针:T const * p 或者 const T * P;
指向常量的常指针:const T *const cpct 或者 T const *const cpct;
对于指针来说,‘*’前面修饰符的顺序对于是否是长指针没有关系。只有在’*‘之后的const才能确定定义的是常量指针。一个常量指针可以指向一个非常量的值,但是一个非常量指针不可以指向一个常量。实际上说起来挺麻烦的实际上就是非常量指针不允许指向一个常量是因为那样做的话就是说可以更改一个常量的值,这显然是错误的。
5.新式转型操作符
c++采用新的转型操作符,舍弃了c里面两种转换类型,本书的作者说,因为类型转换本身就存在危险性,所以采用四个相貌丑陋的操作符来进行操作。
使用方法: T a; TT b=**_cast<TT>(a)
意思是:把括号里面的数据转化成”<>”内的数据类型。
dynamic_cast、static_cast
reinterpret_cast
这四个转型操作符各司其职:
l const_cast 通常用来将对象的常量性转除(cast away the constness)。它是唯一有此能力的C++-style转型操作符。
l dynamic_cast用来执行继承体系中安全的向下转型或跨系转型动作。也就是说你可以利用它将指向基类对象的指针或者引用转型为指向派生类对象的指针或引用,并得知转型是否成功。如果转型失败,会以一个null指针(当转型对象是指针)或一个exception(当转型对象是引用)表现出来。dynamic_cast是唯一无法由旧式语法执行的转型动作,也是唯一可能消耗重大运行成本的转型动作。
l static_cast 基本上拥有与C旧式转型相同的威力与意义,以及相同的限制。例如将一个非 const 的对象转换为 const 对象,或将int转换为 double等等。它也可以用来执行上述多种转换的反向转换,例如将void*指针转为typed指针,将pointer-to-base转为pointer-to-derived。但是他无法将const转为non-const,这个只有const-cast才能够办到。
l reinterpret_cast意图执行低级转型,实际动作及结果可能取决于编译器,这也就表示它不可移植。例如将一个pointer to int 转型为int。这一类转型在低级代码以外很少见。
旧式转型在 C++ 中仍然是合法的,但是这里更推荐使用新形式。首先,它们在代码中更加易于辨认(不仅对人,而且对 grep 这样的工具也是如此),因而得以简化“找出类型系统在哪个地点被破坏”的过程。第二,各转型动作的目标愈窄化,编译器愈可能诊断出错误的运用。例如,如果你打算将常量性去掉,除非使用新式转型中的const_cast,否则无法通过编译
具体的的信息可以查看:http://www.cplusplus.com/doc/tutorial/typecasting/
6.常量成员函数含义
在类T的非常量成员函数中,this指针的类型为T *const,也就是说,它是指向非常量的常量指针;
而在类T的常量成员函数中,this的类型为const T *const,也就是说是指向常量的常量指针。
7.编译器会在类中放东西
在C++中对于一个类来说,它本身所具有的代码不是我们所看到的那样,例如,如果声明了一个或多个虚函数,那么编译其将会为该类的每个对象插入一个指向虚函数表的指针(事实上,标准并没有保证一定如此,但所有的C++编译器都是采用这种方式来实现虚函数机制的),实际上,有些编译器会在类的开始添加一个指向虚函数表的指针;有些编译器将可能会在类的结束添加,而且如果涉及多重继承,若干个虚函数表指针就可能会散布于对象之中。所以永远不要假设你准确的知道虚函数指针的位置。
不管类的数据成员的声明顺序如何,编译器都会允许有节制的重新安排它们的布局。
POD(plain old data)标准,朴素的旧式数据,可以像C struct一样,能够保证C++的编译器不会对其做什么更改。如内建的类型:int float等,还有C struct或者类似union的声明也都是POD。
struct S{ //is a POD int a; float b; }; | struct S{ //is not a POD int a; float b; private: string c; //编译器会对此进行一些维护 }; |
对于这种编译器的插手,我们应该注意:
我们应该在高层次操纵对象,而不应该把它们当成一组位bit的集合。在不同的平台上,高层的操作将会做同样的事情,但它们的底层实现可能并不相同。
例如,要复制一个类对象,那么不要使用memcopy这样标准内存块复制函数,以为这种函数是用来复制存储区的,而不是用来复制对象的;我们因该用对象的初始化或者复制构造函数来操作。
8.复制和初始化并不相同
赋值发生于当你赋值的时候,除此之外,遇到的所有其他的复制情形均为初始化,包括声明、函数返回、参数传递以及捕获异常中的初始化。
int a=12;//初始化 | int a; a=12; //赋值 |
对于int/double这样的内建类型来说,这种操作上的不同并不明显,因为这种情况下,进行的操作不过是简单的复制一些位而已。
但是,对于用户自定义类型来说,情况却不同。
赋值和初始化的本质是不同的,比如对一个String对象来说:
String类如下:
class String
{
public:
String(const char *init);
~String();
String(const String &that);
String &operator=(const String &that);
String &operator=(const char *str);
void swap(String &that);
private:
String(const char*,const char *);
char *s_;
};
下面是初始化构造函数和赋值构造函数的实现代码:
初始化: String::String(const char *init) { if(!init)init =””; s_=new char[strlen(init)+1]; strcpy(s_,init); } 注意:在构造函数中并没有引用一个额外的中间变量 | 赋值: String &Stirng::operator =(const char *str) { if(!str)str=””; chat *temp=strcopy(new char[strlen(str)+1],str); delete []s_; //之所以要先delete是因为赋值操作是在假定 //s_已经经过了初始化或被使用过了 s_=temp; return *this; } 注意:赋值的过程中有中间变量temp *this是代表当前类的对象 |
构造函数比赋值函数做的事情少是因为构造函数可以假定它肯定是在处理一个未初始化的存储区。
在赋值”=“执行的时候,编译器会首先删除/清理左边的目标,对于上面例子来讲就是会先删除*this的内容,然后再赋值。所以,如果你直接用赋值函数来对一个未初始化的对象赋值,编译器会产生错误。
详细的介绍见:http://blog.csdn.net/pcliuguangtao/archive/2010/11/05/5990561.aspx 《”C++中赋值和初始化不同“实例说明》
9.复制操作
复制构造(copy construction):Handle(const Handle &);
复制赋值(copy assignment):Handle &operator =(const Hnadle &);
10.指向类成员的指针并非指针
指向类成员的指针,本质上不是一个指针,因为他既不包含地址,行为也不像一个指针,在多数编译自里面更确切的说是类里面的一个int型偏移量。
int *ip;
int C::*pimC; //一个指向C类的int 成员指针,正是因为貌似一个指针,我们才称他为“指针”
声明一个类成员的指针所要做的就是使用classname::* 来代替普通的*即可。
通常最清晰的做法是将指向数据成员的指针看成是一个偏移量。但是c++标准对于一个指向数据成员的指针该如何实现只字未提,标准只是说明了它的语法形式以及必须表现出来的行为。
对于一个偏移量如果为0表示一个空的数据成员指针。这个偏移量告诉你一个特定的成员距离对象的起点有多少个字节。
class C
{
public:
//…
int a_;
};
int C::*pimC;
C aC;
C *pC=&aC;
pimC=&C::a_;
//两种用数据成员指针访问类成员的方式
aC.*pimC=0; int b=pC->*pimC;
将pimC设置为&C::a_时,实际上是将pimC设置为a_在C内的偏移量。更详细点说,除非a_是静态成员,否则&C::a_中使用&并不会带来一个地址,而是一个偏移量。
11.指向成员函数的指针并非指针
class C
{
public:
//…
void moveTo(Point destination);
};
void (C::*mf1)(Point)=&C::moveTo; //声明一个指向成员函数的指针mf1
通常,一个指向成员函数的指针,通常不能被简单的实现为一个指向函数的指针。一个指成员向函数的指针必须存储一些信息,诸如它所指向的成员函数是否为虚拟的,到哪里去找到虚函数表,从函数的this指针加上或者减去一个偏移量,以及可能还有其他的一些信息。指向成员函数的指针通常实现为一个小型的结构,其中包含这些信息。
11.命名空间
全局作用域日益变得拥挤,把所有的函数都归为一个作用域中将会使函数名变得越来越丑陋难记。命名空间本质上是对全局作用域的细分。
namespace org_segmantics
{
class Stirng{……};
String operator +(const String &,const String &);
//other function ,classes types definations
}
这段代码建立了一个名为org_segmantics的命名空间。下面介绍怎样像一个命名空间中添加内容:
- 直接打开: namespace org_segmanti{String operator+ (const String &a,const String &b) {//…….} ……}
- 加上命名空间限制: org_segmantics::String/ org_segmantics::operator +(const String &a,const String &b){//…..}
使用一个命名空间中的成员时,可以直接在该成员前面加上 namespace::,如果想让那个命名空间作为默认的名字空间的话,可以在文件头不添加如下代码:
using namespace namespace;
ex: using namspace std; //把std作为默认命名空间
匿名名字空间(anonymous namespace)
namespace{
int anint=12;
int Func(){return anint;}
}
上面的代码与下面的代码作用完全一样,对于每一个命名空间,都有一个唯一的“_compiler_generated_name_”
namespace _compiler_generated_name_
{
int anint=12;
int Func(){return anint;}
}
using namespace _compiler_generated_name_;
这是一个避免声明具有静态链接的函数和变量的新潮方式。在上面的匿名命名空间中,anint 和Func都具有外部链接,但他们只能在其所出现的预处理后的文件内被访问,就像静态对象一样。
12.成员函数的查找
- 编译器查找函数的名字
- 从可用后选择最佳匹配函数
- 检查是否具有访问该匹配函数的权限
13.实参相依的查找
名字空间对现代C++编程和设计有着深远的影响,有些影响直接而明显,如using声明;除此之外还有一些不明显但仍然很基础的、很重要的影响。ADL(Argument Dependent Lookup)实参相依就是其中之一。
ADL背后蕴涵的思想非常简单:当查找一个函数调用表达式中的函数名字时,编译器也会到“包含函数调用实参的类型”的名字空间中检查。例如:
namespace org_semantics{
class X{…};
void f(const X &);
void g(X *);
X operator +(const X&,const X&);
class String{…};
std::ostream operator <<(std::ostream &,const String &);
}
//….
int g(org_semantics::X *);
void Func()
{
org_semantics::X a;
f(a); //根据a 的类型确定调用org_semantics::f()
g(&a); //错误,调用具有歧义性
a=a+a; //调用org_semantics::operator +
}
普通的查找是不会发现org_semantics::f()的,因为他被嵌套在一个名字空间内,并且对f的调用需要用该名字空间的名字加以限定。然而由于实参a的类型被定义于org_semantics名字空间中,因此编译器也会到改名字空间中检查候选函数。
另外注意::f()并没有使org_semantics::f()重载,因为二者不在同一个命名空间中。
14.能力查询
当我们碰到不知道一个对象是否具有所需能力的情形时。我们被迫执行一个能力查询。
例如查看一个Shape型的对象是否可滚动形状:
Shape * s= getSomeShape();
Rollable *roller=dynamic_cast<Rollable *>(s);
这种dynamic_cast用法通常称为”横向转型(cross-cast)”,因为它视图在一个类层次结构中执行横向转换,而不是向上或向下想转换。
例如:
class Shape{….};
class Subject{…};
class ObservedBlod:public Shape,public Subject{…};
ObservedBlod *ob=new ObservedBlod;
Shape *s=ob ;//预定义转换
Subject *subj=ob ;//预定义转换
下面两个判断语句全部为真,即使ob/s/subj中包含的地址并不相同:
if(ob==s)
if(subj==ob)
16.虚构造函数和Prototype模式
如果你去外国的一家餐厅吃饭,但是你有不太会说菜名,但是你偶然看到一位正在用餐的先生所吃的东西正好符合你的胃口,这时候你可以告诉服务生,你需要一份和某一位先生一样的食物。这样你就很简单的达到了自己的目的,与此同时对于你所点的菜到底叫什么名字确有一定的不知情。虚拟构造函数的作用与此类似:当你想要获得目标类的一个对象,但是你对该类所知甚少,这时候如果目标类定义有虚拟构造函数的话你就可以直接克隆出一个一样的对象。虚拟构造函数实际不是构造函数,它有返回值,并且只是对其累得快构造函数的调用,因此,即使不是真的虚构造函数,效果上也算是虚构造函数了。这种技术被称为Prototype模式的一个实例。
例如:
class Meal
{
public:
virtual ~Meal();
virtual void eat()=0;
virtual Meal *clone() const = 0;
/….
};
class ChinaMeal:public Meal
{
public:
ChinaMeal(const ChinaMeal &); //复制构造函数
void eat();
ChinaMeal *clone() const
{
return new ChinaMeal(*this); //调用复制构造函数
}
//….
}
虚拟复制构造函数部分的反映了软件设计中“不知情”的一些优点,尤其是在用于定制和扩展的框架结构软件设计中。有些事情知道的越少越好~~