C++ 学习总结

总结一

包括以下内容:new关键字、抽象类、类成员变量初始化、函数指针、虚函数表解析、虚函数与重载函数区别、重载重写覆盖、基类与派生类对象的指针赋值、extern关键字、inline内联函数。

new在创建类中的使用

new在创建类时确实有很多好处,但是也有局限性,比如在频繁调用时局部new类对象就显得效率很低。

new主要带来以下几点:

  • new创建类对象需要指针接收,一处初始化,多处使用;
  • new创建类对象使用完需要delete销毁;
  • new创建类对象之间使用堆空间,而局部不用new定义的类对象则使用栈空间;
  • new对象指针用途广泛,比如作为函数返回值、函数参数等
  • 频繁调用场合并不适用new

再谈抽象类

不用来定义对象而只作为一种基类型用作继承的类,称为抽象类(abstract class),凡是包含纯虚函数(pure virtual function)的类都是抽象类。

因为纯虚函数是不能被调用的,包含纯虚函数的类是无法建立对象的。

抽象类的作用是作为一个类族的共同基类,或者说是一个类族的一个公共接口。

如果在抽象类所派生出来的新类中对基类的所有纯虚函数进行了定义,那么这些函数就被赋予了功能,可以被调用。派生类就是具体类(concrete class)。如果在派生类中没有对所有纯虚函数进行定义,则此派生类仍然是抽象类,不能用来定义对象。

虽然抽象类不能定义对象(或者说抽象类不能实例化),但是可以定义指向抽象类数据的指针变量。当派生类成为具体类之后,就可以用这种指针指向派生类对象,然后通过该指针调用虚函数,实现多态性的操作。

类成员变量初始化总结

常用的有5种方法:

  1. 在无参数的构造函数中初始化;
  2. 带参数的构造函数中初始化;
  3. 直接给成员变量赋值;
  4. 调用成员函数来初始化成员变量;
  5. this指针。

针对不同的变量类型,在选择初始化方法时,有不同的优先顺序:

  • 普通的变量
    一般不考虑效率的情况下可以在构造函数中进行赋值。考虑效率的可以在构造函数的初始化列表中进行。

  • static 静态变量
    类外进行初始化。static变量属于类所有,而不属于类的对象,因此不管类被实例化了多少个对象,该变量都只有一个。在这种性质上理解,有点类似于全局变量的唯一性。

  • const 常量变量
    const常量需要在声明的时候即初始化,因此需要在变量创建的时候进行初始化,必须采用在构造函数的初始化列表中进行。

  • & 引用型变量
    引用型变量和const变量类似,需要在创建的时候即进行初始化,也是必须在初始化列表中进行。

  • const static integral 变量
    对于既是const又是static而且还是整形变量,C++是给予特权的。可以直接在类的定义中初始化。short可以,但float的不可以。

总结起来:

  1. 在类的定义中进行的,只有conststaticintegral的变量。
  2. 在类的构造函数初始化列表中,包括普通变量,const常量(不包含第一种情况)和引用变量(&)。
  3. 在类的定义之外初始化的,包括static变量。因为它是属于类的唯一变量。
  4. 普通的变量可以在构造函数的内部,通过赋值方式进行。当然这样效率不高。
  5. const数据成员(非static)必须在构造函数的初始化列表中初始化。
  6. 数组成员是不能在初始化列表里初始化的。
  7. const staticstatic const是一样的,这样的变量可以直接在类定义中初始化,也可以在类外。 说明了一个问题:C++里面是不能定义常量数组的!因为5和6的矛盾。

类对象的构造顺序是这样的:

  1. 分配内存,调用构造函数时,隐式/显示的初始化各数据成员 ;
  2. 进入构造函数后在构造函数中执行一般计算。

函数指针

函数指针:一个指向函数的指针,表示一个函数的入口地址,可以在运行时根据数据的具体状态来选择相应的处理方式。

在动态调用DLL的函数时会用到函数指针。最典型的是回调函数。

回调函数其实就是一个通过函数指针调用的函数!假如你把A函数的指针当作参数传给B函数,然后在B函数中通过A函数传进来的这个指针调用A函数,那么这就是回调机制。A函数就是回调函数,而通常情况下,A函数是系统在符合你设定条件的情况下会自动执行,比如Windows下的消息触发等等。那么调用者和被调用者的关系就被拉开了,就像是中断处理函数那样。

函数指针应该能够指向对应类型的任何变量。而函数的类型靠这几方面来确定:(1)函数的参数个数 (2)函数的参数类型(3)函数的返回值类型。

C语言中的定义

返回类型 (*函数指针名称)(参数类型,参数类型……);

C++中的定义

返回类型 (类名称::*函数成员名称) (参数类型,参数类型……)

Tips:
回调函数必须是全局函数或者静态成员函数,因为普通的成员函数会隐含着一个传递函数作为参数,也就是this指针。因此如果使用普通成员函数作为回调函数的话会导致函数参数个数不匹配,因此编译失败。这也是线程函数是多为静态函数的原因。
我们还注意到回调函数用CALLBACK修饰,我们可以在windef.h中发现:

1
#define CALLBACK    __stdcall

CALLBACK其实就是__stdcall,还记得上篇讲过的函数调用约定吗?

虚函数表解析

编译器会为每个有虚函数的类创建一个虚函数表,该虚函数表将被该类的所有对象共享。在有虚函数的类的实例中分配了指向这个表的指针。

虚函数表的指针存在于对象实例中最前面的位置。通过对象实例的地址得到这张虚函数表,然后就可以遍历其中函数指针,并调用相应的函数。

虚函数与重载函数的区别

  • 重载函数在类型和参数数量上一定不相同,而重定义的虚函数则要求参数的类型和个数、函数返回类型相同;
  • 虚函数必须是类的成员函数,重载的函数则不一定是这样;
  • 构造函数可以重载,但不能是虚函数,析构函数可以是虚函数。

例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class A
{
public:
virtual int f(unsigned char ch) {return --ch;}
};
class B : public A
{
int f(char ch) {return ++ch;} //此为函数重载
};
void main()
{
A* p=new B;
int n=p->f(40); //调用基类的 f()
cout<<" the result is : "<<n<<endl;
}

运行结果:the result is : 39

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class A
{
public:
virtual int f(unsigned char ch) {return --ch;}
};
class B : public A
{
int f(unsigned char ch) {return ++ch;} //此为虚函数
};
void main()
{
A *p=new B;
int n=p->f(40); //调用派生类的 f()
cout<<" the result is : "<<n<<endl;
}

运行结果:the result is : 41

重载、重写、覆盖

一、重载(overload)
指函数名相同,但是它的参数表列个数或顺序,类型不同。但是不能靠返回类型来判断。
(1)相同的范围(在同一个作用域中) ;
(2)函数名字相同;
(3)参数不同;
(4)virtual 关键字可有可无。
(5)返回值可以不同;

二、重写(也称为覆盖 override)
是指派生类重新定义基类的虚函数,特征是:
(1)不在同一个作用域(分别位于派生类与基类) ;
(2)函数名字相同;
(3)参数相同;
(4)基类函数必须有 virtual 关键字,不能有 static 。
(5)返回值相同(或是协变),否则报错;
(6)重写函数的访问修饰符可以不同。尽管 virtual 是 private 的,派生类中重写改写为 public,protected 也是可以的

三、重定义(也称隐藏)
(1)不在同一个作用域(分别位于派生类与基类) ;
(2)函数名字相同;
(3)返回值可以不同;
(4)参数不同。此时,不论有无 virtual 关键字,基类的函数将被隐藏(注意别与重载以及覆盖混淆) 。
(5)参数相同,但是基类函数没有 virtual关键字。此时,基类的函数被隐藏(注意别与覆盖混淆) 。

基类与派生类对象的指针赋值

派生类对象也基类对象,但两者不同。

派生类对象可以当做基类对象,这是因为派生类包含基类的所有成员。

但是基类对象无法被当做成派生类对象,因为派生类可能具有只有派生类才有的成员。

所以,将派生类指针指向基类对象的时候要进行显示的强制转换,否则会使基类对象中的派生类成员成为未定义的。

基类指针和派生类指针指向基类对象和派生类对象的方法:

  1. 基类指针指向基类对象,只需要通过基类指针简单地调用基类的功能。
  2. 派生类指针指向派生类对象,只需要通过派生类指针简单地调用派生类功能。
  3. 基类指针指向派生类对象是安全的,因为派生类对象“是”它的基类的对象。但是要注意的是,这个指针只能用来调用基类的成员函数。
    如果试图通过基类指针调用派生类才有的成员函数,则编译器会报错。
    为了避免这种错误,必须将基类指针强制转化为派生类指针。然后派生类指针可以用来调用派生类的功能。这称为向下强制类型转换,这是一种潜在的危险操作。
    注意:如果在基类和派生来中定义了虚函数(通过继承和重写),并通过基类指针在派生类对象上调用这个虚函数,则实际调用的是这个函数的派生类版本。
  4. 派生类指针指向基类对象, 基类对象并不包含派生类才有的成员,这些成员只能通过派生类指针调用。

extern关键字

extern可以置于变量或者函数前,以标示变量或者函数的定义在别的文件中,提示编译器遇到此变量和函数时在其他模块中寻找其定义。

extern是C/C++语言中表明函数和全局变量作用范围(可见性)的关键字。它告诉编译器,其声明的函数和变量可以在本模块或其它模块中使用。

  1. 对于extern变量来说,仅仅是一个变量的声明,其并不是在定义分配内存空间。如果该变量定义多次,会有连接错误;
  2. 通常,在模块的头文件中对本模块提供给其它模块引用的函数和全局变量以关键字extern声明。也就是说c文件里面定义,如果该函数或者变量与开放给外面,则在h文件中用extern加以声明。所以外部文件只用include该h文件就可以了。
  3. extern对应的关键字是static,被它修饰的全局变量和函数只能在本模块中使用。

内联函数 inline

定义:内联函数从源代码层看,有函数的结构,而在编译后,却不具备函数的性质。内联函数不是在调用时发生控制转移,而是在编译时将函数体嵌入在每一个调用处。编译时,类似宏替换,使用函数体替换调用处的函数名。

作用:内联扩展是用来消除函数调用时的时间开销。它通常用于频繁执行的函数。 一个小内存空间的函数非常受益。

建议把inline函数的定义放到头文件中。在每个调用该inline函数的文件中包含该头文件。这种方法保证对每个inline函数只有一个定义,且程序员无需复制代码,并且不可能在程序的生命期中引起无意的不匹配的事情。

推荐编程风格:

1
2
3
4
5
6
7
8
9
// 头文件
class A
{
public:
void Foo(int x, int y);
}

// 定义文件
inline void A::Foo(int x, int y){}

总结二

包括以下内容:红黑树、函数对象的函数适配器、explicit关键字、const关键字、类类型转换、虚析构函数

红黑树

红黑树(RB-tree)是一个平衡二叉搜索树。

定义:
每个节点不是红色就是黑色;根节点是黑色;如果节点为红,其子节点必须为黑;任一节点至空节点的任何路径,所含之黑节点数必须相同。

##函数对象的函数适配器

标准库提供了一组函数适配器(function adapter),用于特化和扩展一元和二元函数对象。

  1. 绑定器(binder):通过将一个操作数绑定到给定值而将二元函数对象转换为一元函数对象。
  2. 求反器(negator):将谓词函数对象的真值求反。

例:计算一个容器中所有小于或等于10的元素的个数,可以这样给count_if传递值:

标准库定义了两个binder

  • bind1st,将给定值绑定到二元函数对象的第一个实参;
  • bind2nd,将给定值绑定到二元函数对象的第二个实参。
1
2
//对小于等于10的值计数
count_if(vec.begin(), vec.end(), bind2nd(less_equal<int>(), 10));

标准库定义了两个negator

  • not1,将一元函数对象的值求反;
  • not2,将二元函数对象的值求反。
1
2
//对不小于等于10的值计数
count_if(vec.begin(), vec.end(), bot1(bind2nd(less_)));

explicit关键字

explicit的意思是明显的,和它相对应的一个词是implicit,意思是隐藏的

explicit只能用在类构造函数,它的作用是不能进行隐式转换

举例说明:

当不用explicit时,默认是implicit

1
2
3
4
5
6
7
8
9
10
class myclass
{
public:
myclass(int size)
{
_size = size;
}
private:
int _size;
}

在调用时:

1
2
myclass a(1);	//这样是没有问题的
muclass a = 1; //这样是没有问题的

第二条看起来很奇怪,但确实是合法的,因为编译器默认有隐式转换,在构造函数只有一个参数时第二条就被转换成和第一条一样。

当加上explicit时:

1
2
3
4
5
6
7
8
9
10
class myclass
{
public:
explicit myclass(int size)
{
_size = size;
}
private:
int _size;
}

在调用时:

1
2
myclass a(1);	//这样是没有问题的
myclass a = 1; //编译器报错

这时第二条不会转换,所以编译器报错。

Tips:

当构造函数的实际参数超过一个时,默认是explicit


explicit myclass(int size, int age) {}

myclass(int size, int age) {}
是等效的。

但是有一个例外,如果只需要一个参数,而其他参数都有默认值时,默认的是implicit


myclass(int size, int age = 0) {}

explicit myclass(int size, int age = 0) {}
是不一样的。

const关键字

const需要注意以下几点:

  • 声明的变量只能被读,不能被赋值;
  • 声明后必须初始化;
  • extern const在另一个文件引用不能再次赋值;
  • 其实可以强制类型转换为指针,通过指针修改const常量,但是要慎用;
  • const int*表示指针所指内容是常量,int* const表示指针是常量,const int* const表示指针和所指内容都是常量。

用法:

  1. const定义常量,比宏常量的优点是有类型,可以方便编译器检查,减少一些bug;
  2. 修饰类的数据成员,不能在类声明时初始化,只能在类的构造函数的初始化表中初始化;
  3. 修饰指针
    总结:
    (1) 指针本身是常量不可变
    (char*) const pContent;
    const (char*) pContent;
    (2) 指针所指向的内容是常量不可变
    const (char) *pContent;
    (char) const *pContent;
    (3) 两者都不可变
    const char* const pContent;
  4. 修饰函数参数,传递过来的参数在函数内不可改变,一般和引用&一起使用;
  5. 修饰函数返回值
  6. const常量与define宏定义的区别
    (1) 编译器处理方式不同
    define宏是在预处理阶段展开。
    const常量是编译运行阶段使用。
    (2) 类型和安全检查不同
    define宏没有类型,不做任何类型检查,仅仅是展开。
    const常量有具体的类型,在编译阶段会执行类型检查。
    (3) 存储方式不同
    define宏仅仅是展开,有多少地方使用,就展开多少次,不会分配内存。
    const常量会在内存中分配(可以是堆中也可以是栈中)。

类类型转换

作用:转换可以减少所需要操作符的数目。

转换操作符定义:

1
2
3
4
5
6
7
8
9
class
{
public:
operator int() const { return val; }
//表示转换为int
//转换函数一般不应该改变被转换对象,所以通常用const
private:
std::size_t val;
}

更一般的形式:

1
2
operator type();
//type表示内置类型名、类类型名或由类型别名所定义的名字

Tips:

  • 被转换的类型不必与所需要的类型完全匹配,可以在类类型转换后跟随标准转换;
  • 但是只允许一次类类型转换,不能一次类类型转换后跟另一个类类型转换;
  • 避免转换函数的过度使用,避免编写互相提供隐式转换的成对的类。

关于重载转换

  • 如果类既定义了转换操作符又定义了重载操作符,容易产生二义性;
  • 不要定义相互转换的类;
  • 不要定义接受算术类型的操作符的重载版本;
  • 不要定义转换到一个以上算术类型的转换。

虚析构函数

一般来说,基类的析构函数多为虚析构函数。

这样做的好处就是,当一个基类指针指向派生类时,释放指针时会调用派生类的析构函数,如果基类的析构函数不是虚析构函数,那么就不会调用,可能导致内存泄露。

例:

1
2
3
4
5
6
7
8
9
class A{
A();
virtual ~A() { cout << "~A()" << endl; }
virtual void foo() { cout << "A::foo" << endl; }
}
class B : public A {
~B() { cout << "~B()" << endl;}
void foo() { cout << "B::foo" << endl;}
}

运行:

1
2
3
A *p = new B;
p->foo();
delete p;

结果:

B::foo
~B()
~A()

如果把 ~A()前面的virtual去掉的话,结果:

B::foo
~A()

总结三

包括以下内容:智能指针、has-a设计与is-a设计、auto关键字、volatile关键字、mutable关键字

智能指针

auto_ptr定义在memory头文件中,接受一个类型参数的模板,为动态分配的对象提供异常安全。

  • auto_ptr不能管理动态分配的数组,所以不能将auto_ptr存储在标准库容器类型中;
  • auto_ptr对象超出作用域或者另外撤销的时候,就自动回收auto_ptr所指向的动态分配对象;
  • auto_ptr可以在newdelete之间发生异常时回收内存;
  • auto_ptr可以保持任何类型;
  • 不能直接将一个地址赋值给auto_ptr对象;
  • 接受指针的构造函数为explicit构造函数。

初始化:

因为构造函数为explicit,所以必须使用初始化的直接形式来创建auto_ptr对象:

1
2
auto_ptr<int> pi = new int(1024);//错误,无法自动完成int到auto_ptr的转换
auto_ptr<int> pi(new int(1024));//正确,直接用new初始化

复制和赋值:

auto_ptr的复制和赋值改变右操作数,即在复制或赋值时把基础对象的所有权从原来的auto_ptr对象转给副本,原来的auto_ptr对象重置为未绑定

赋值会删除左操作数指向的对象,例:

1
2
3
auto_ptr<int> ap1(new int(1));
auto_ptr<int> ap2(new int(2));
ap1 = ap2;

ap2赋值给ap1后:

  • 删除了ap1所指向的对象;
  • ap1置为指向ap2所指的对象;
  • ap2是未绑定的auto_ptr对象。

测试auto_ptr对象:

如果不给定初始值,auto_ptr对象是未绑定的,默认情况下,auto_ptr的内部指针值为0

为了检查auto_ptr是否绑定,不能直接测试,要用get成员.

1
2
3
4
5
6
auto_ptr<int> ptr;	//未绑定
if(ptr.get()) //用get来判断,未绑定返回0
*ptr = 2;
else
ptr.reset(new int (3)); //只能用reset重设
//要复位auto_ptr对象可以将0传给reset函数

缺陷:

要正确使用auto_ptr类,必须坚持该类强加的下列限制:

  1. 不要使用auto_ptr对象保存指向静态分配对象的指针,否则,当auto_ptr对象本身被销毁的时候,它将试图删除指向非动态分配对象的指针,导致未定义的行为;
  2. 不要使用两个auto_ptr对象指向同一对象,不要使用同一指针来初始化或者reset两个不同的auto_ptr对象,不要使用一个auto_ptr对象的get函数的结果来初始化或者reset另一个auto_ptr对象。
  3. 不要使用auto_ptr对象保存指向动态分配数组的指针,因为auto_ptr对象被删除时调用的是普通delete操作符,而不是数组的delete[]操作符;
  4. 不要讲auto_ptr对象存储在容器中。

has-a设计与is-a设计

has-a:基于用法(即引用)而不是继承,表示组合,包含关系。一个对象包含另一个对象

is-a:基于类继承或接口实现,示的是属于关系。具体类是接口的一种实现。

is-a是继承,has-a是组合

auto关键字

作用:C++ 11中引入的auto主要有两种用途:自动类型推断返回值占位

auto声明的变量必须被初始化,以使编译器能够从其初始化表达式中推导出其类型。这个意义上,auto并非一种类型声明,而是一个类型声明时的占位符

auto推导的一个最大的优势在于拥有初始化表达式的复杂类型变量声明时简化代码

可以避免类型声明时的麻烦而且避免类型声明时的错误

auto与模板一起使用时,其“自适应”特性能够加强C++中泛型的能力

Tips:

  • auto声明的变量必须初始化;

  • auto不能与其他类型组合连用;

  • 函数和模板参数不能被声明为auto

  • auto是一个占位符,并不是一个他自己的类型,因此不能用于类型转换或其他一些操作;

  • auto会退化成指向数组的指针,除非被声明为引用。

    1
    2
    3
    4
    5
    int a[9];  
    auto j = a;
    cout << typeid(j).name() << endl; // This will print int*
    auto& k = a;
    cout << typeid(k).name() << endl; // This will print int [9]

auto返回值占位,主要与decltype配合使用,用于返回值类型后置时的占位。

1
2
3
4
5
6
7
template <class T, class U>
auto Multiply(T t, U u)->decltype(t*u)
{
typedef decltype(t*u) NewType;
NewType *pResult = NewType(t*u);
return *pResult;
}

至于为什么需要将返回值类型后置,这里简单说明一下。如果没有后置,则函数声明为decltype(t*u) Multiply(T t, U u),但此时模板参数t和u还未声明,编译无法通
过。另外,如果非要使用返回值类型前置的形式,也可以将函数声明为decltype((*(T *)0)*(*(U *)0)) Multiply(T t, U u),但这种形式比较晦涩难懂,因此不推荐采用。

参考:
http://www.cnblogs.com/hujian/archive/2012/02/15/2352050.html

volatile关键字

volatile的本意是易变的volatile关键字是一种类型修饰符,用它声明的类型变量表示可以被某些编译器未知的因素更改,比如操作系统、硬件或者其它线程等。

遇到这个关键字声明的变量,编译器对访问该变量的代码就不再进行优化,从而可以提供对特殊地址的稳定访问。

当要求使用volatile声明的变量的值的时候,系统总是重新从它所在的内存读取数据,即使它前面的指令刚刚从该处读取过数据。而且读取的数据立刻被寄存。

一个定义为volatile的变量是说这变量可能会被意想不到地改变,这样,编译器就不会去假设这个变量的值了。精确地说就是,优化器在用到这个变量时必须每次都小心地重新读取这个变量的值,而不是使用保存在寄存器里的备份。下面是volatile变量的几个例子:

  1. 并行设备的硬件寄存器(如:状态寄存器
  2. 一个中断服务子程序中会访问到的非自动变量(Non-automatic variables)
  3. 多线程应用中被几个任务共享的变量

问题:

  • 一个参数既可以是const还可以是volatile吗?解释为什么。
    是的。一个例子是只读的状态寄存器。它是volatile因为它可能被意想不到地改变。它是const因为程序不应该试图去修改它。

  • 一个指针可以是volatile吗?解释为什么。
    是的。尽管这并不很常见。一个例子是当一个中服务子程序修该一个指向一个buffer的指针时。

  • 下面的函数有什么错误?

    1
    2
    3
    4
    int square(volatile int *ptr)
    {
    return *ptr * *ptr;
    }

    这段代码的有个恶作剧。这段代码的目的是用来返指针ptr指向值的平方,但是,由于ptr指向一个volatile型参数,编译器将产生类似下面的代码:

    1
    2
    3
    4
    5
    6
    7
    int square(volatile int *ptr)
    {
    int a,b;
    a = *ptr;
    b = *ptr;
    return a * b;
    }

    由于*ptr的值可能被意想不到地该变,因此ab可能是不同的。结果,这段代码可能返不是你所期望的平方值!正确的代码如下:

1
2
3
4
5
6
long square(volatile int *ptr)
{
int a;
a = *ptr;
return a * a;
}

参考:
C++中const、volatile、mutable的用法

mutable关键字

mutalbe的中文意思是“可变的,易变的”,跟constant(既C++中的const)是反义词。

C++中,mutable也是为了突破const的限制而设置的。被mutable修饰的变量(mutable只能由于修饰类的非静态数据成员),将永远处于可变的状态,即使在一个const函数中。

假如类的成员函数不会改变对象的状态,那么这个成员函数一般会声明为const。但是,有些时候,我们需要在const的函数里面修改一些跟类状态无关的数据成员,那么这个数据成员就应该被mutalbe来修饰。

参考:

C++基本功: 全面掌握const, volatile 和 mutable关键字

总结四

包括以下内容:构造函数中调用虚函数、STL算法之for_each、vector中的remove、erase的用法及在map中的陷阱、inserter用法、remove_copy_if用法、transform函数、map::lower_bound、计算机的大小端。

#构造函数中调用虚函数

首先先看一个列子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
class Base
{
public:
Base()
{
Fuction();
}

virtual void Fuction()
{
cout << "Base::Fuction" << endl;
}
};

class A : public Base
{
public:
A()
{
Fuction();
}

virtual void Fuction()
{
cout << "A::Fuction" << endl;
}
};

// 这样定义一个A的对象,会输出什么?
A a;

调用当然是没有问题的,但是会得到什么样的结果?

首先我们回顾下C++对象模型里面的构造顺序,在构造一个子类对象的时候,首先会构造它的基类,如果有多层继承关系,实际上会从最顶层的基类逐层往下构造。

在构造Base的时候,也就是在Base的构造函数中调用Fuction的时候,不会调用子类AFuction,因为A还没有开始构造,这样函数的行为就是完全不可预测的。

实际的输出结果是:

1
2
Base::Fuction
A::Fuction

同样,不要在类的构造或者析构过程中调用虚函数,因为这样的调用永远不会沿类继承树往下传递到子类中去。

参考:

C++中构造函数能调用虚函数吗?

#STL算法之for_each

功能:

for_each用于逐个遍历容器元素,它对迭代器区间[first,last)所指的每一个元素,执行由单参数函数对象f所定义的操作。

原形:

1
2
3
4
5
6
template<class InputIterator, class Function>
Function for_each(
InputIterator _First,
InputIterator _Last,
Function _Func
);

说明:

for_each 算法范围 [first, last) 中的每个元素调用函数 F,并返回输入的参数f。此函数不会修改序列中的任何元素。

示例:

取自《Effective STL》第20条

1
2
3
4
5
6
set<String*> ssp;
void print(const string *ps)
{
cout << *ps << endl;
}
for_each(ssp.begin(), ssp.end(), print);

#vector中的remove

定义:

1
2
3
4
template <class ForwardIterator, class T>
ForwardIterator remove (ForwardIterator first,
ForwardIterator last,
const T& val);

功能:

移除firstlast之间的值为val的元素。

使用:

注意,remove只是移除,而不是删除,不会改变vector的大小。

比如:

remove前:1, 2, 3, 4, 5

使用remove(ve.begin(), ve.end(), 3);

remove后:1, 2, 4, 5, 5

发现只是移除3,3后面的元素依次前移,导致最后多出一个5。

正确应该配合erase使用:

1
ve.erase(remove(ve.begin(), ve.end(), 3), ve.end());

#erase的用法及在map中的陷阱

定义:

vector中定义如下:

1
2
iterator erase (const_iterator position);
iterator erase (const_iterator first, const_iterator last);

使用:

删除第一个元素:

1
ve.erase(ve.begin());

删除第二个到第四个元素:

1
ve.erase(ve.begin() + 1, ve.begin() + 4);

在map中erase的重载多了一个:

1
size_type erase (const key_type& k);

可以看出map中可以通过erase可以直接删除特定元素。

但是当条件删除时:

1
2
3
4
5
for(it = m.begin(); it != m.end(); ++it)
{
if(*it > 10)
m.erase(it);
}

这样子做会发现出错了。

因为在m.erase(it)后,it所指的对象被删除,it就成为无效值,再进行++it时就会出错。换一种写法就能解决:

1
2
3
4
5
6
7
for(it = m.begin(); it != m.end();)//for循环的第三步什么也不做
{
if(*it > 10)
m.erase(it++);//利用++保证it指向被删除的后一个对象
else
++it;
}

#inserter用法

定义:

1
2
3
4
5
template<class Container>
insert_iterator<Container> inserter(
Container& _Cont,
typename Container::iterator _Where
);

功能:

返回容器_Cont中的位置_Where

  • _Cont:新元素将被添加的容器。
  • _Where:定位点插入的迭代器。

替代insert_iterator<Container>(_Cont,_Where)

例子:

list L的后面插入500

1
2
list<int> L;
inserter (L, L.end()) = 500;

#remove_copy_if用法

定义:

1
2
3
4
5
6
7
template<class InputIterator, class OutputIterator, class Predicate> 
OutputIterator remove_copy_if(
InputIterator _First,
InputIterator _Last,
OutputIterator _Result,
Predicate _Pred
);

功能:

从一个源区的元素复制到目标范围,除非满足布尔函数(predicate),则不复制它们,而其余元素的顺序和返回新的目标范围的末尾。

参数:

  • _First:处理第一元素位置的输入迭代器在中移除元素的范围。
  • _Last:寻址最终元素的输入迭代器位置移除一个元素的范围。
  • _Result:解决的第一个元素的输出位置的迭代器。元素移除的目标范围。
  • _Pred:必须满足的一元谓词是元素的值来替换。
  • 返回值:处理目标范围的新结束位置的前向迭代器。

用法:

取自《Effective STL》第9条

删除关联容器中满足badValue的值:

1
2
3
4
5
6
7
AssocContainer<int> c;//c是一个标准关联容器
...
AssocContainer<int> goodValues;//保存不被删除值的临时容器
remove_copy_if(c.begin(), c.end(),
inserter(goodValues, goodValues.end()),
badValue);//不被删除的值放入goodValue
c.swap(goodValues);//交换c和goodValue的内容

#transform函数

作用
将某操作应用于指定范围的每个元素。

定义:

1
transform(first,last,result,op);

first是容器的首迭代器,last为容器的末迭代器,result为存放结果的容器,op为要进行操作的一元函数对象或sturctclass

1
transform(first1,last1,first2,result,binary_op);

first1是第一个容器的首迭代器,last1为第一个容器的末迭代器,first2为第二个容器的首迭代器,result为存放结果的容器,binary_op为要进行操作的二元函数对象或sturctclass

#map::lower_bound

定义:

1
2
iterator lower_bound (const key_type& k);
const_iterator lower_bound (const key_type& k) const;

功能:

返回一个迭代器,指向第一个不满足key_comp函数的元素。

类似的有map::upper_bound

例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
std::map<char,int> mymap;
std::map<char,int>::iterator itlow,itup;

mymap['a']=20;
mymap['b']=40;
mymap['c']=60;
mymap['d']=80;
mymap['e']=100;

itlow=mymap.lower_bound ('b'); // itlow points to b
itup=mymap.upper_bound ('d'); // itup points to e (not d!)

mymap.erase(itlow,itup);

删除后,map中只有:
a 20
e 100

#计算机的大小端

定义:

  • Little-Endian就是低位字节排放在内存的低地址端,高位字节排放在内存的高地址端。
  • Big-Endian就是高位字节排放在内存的低地址端,低位字节排放在内存的高地址端。

比如:

32bit宽的数0x12345678

内存地址 小端模式存放内容 大端模式存放内容
0x4000 0x78 0x12
0x4001 0x56 0x34
0x4002 0x34 0x56
0x4003 0x12 0x78

常见的字节序:

一般操作系统都是小端,而通讯协议是大端的。

大端模式CPU : PowerPC、IBM、Sun

小端模式CPU: x86、DEC

ARM既可以工作在大端模式,也可以工作在小端模式。

优缺点:

  • 小端模式 :强制转换数据不需要调整字节内容,1、2、4字节的存储方式一样。
  • 大端模式 :符号位的判定固定为第一个字节,容易判断正负。

参考:

详解大端模式和小端模式

总结五

包括以下内容:数组指针和指针数据、虚继承、下标运算符、boost 智能指针、不可以被声明为虚函数、不可被重载的运算符、4种类型转换cast、typeid操作符

数组指针和指针数据

指针数组:

一个数组,其元素均为指针型数据,即指针数组中的每一个元素相当于一个指针变量,它的值都是地址。

1
2
int *p[4];
int* p[4];

由于[ ]*优先级高,因此p先与[4]结合,形成p[4]的数组的形式。

数组指针:

一个指针变量,它不指向一个整型变量,而是指向一个包含m个元素的一维数组。

1
int (*p)[4];

指向包含4个整形元素的一维数组。

1
2
3
int a[3][4] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12};
int (*p)[4];
p = a;

使p指向a[0],则p+1不是指向a[0][1],而是指向a[1]p的增值以一位数组的长度为单位。

因此,*(p+1)+1 == &a[1][1]

参考:

C++指针数组和指向指针的指针
多维数组与指针

虚继承

定义:

虚继承是一种机制,类通过虚继承指出它希望共享虚基类的状态。对给定的虚基类,无论该类在派生层次中作为虚基类出现多少次,只继承一个共享的基类子对象,共享基类子对象称为虚基类。

虚拟继承是多重继承中特有的概念。

1
2
3
4
5
6
7
8
class A
{
virtual void f(){}
};
class B : public virtual A
{
virtual void f1(){}
};

当求sizeof(a)sizeof(b)时,分别为4,12。

因为每个存在虚函数的类都要有一个4字节的指针指向自己的虚函数表,所以每种情况的类a所占的字节数应该是4,类b采用的是虚继承,那么这时候就要有这样的一个指针vptr_b_a,这个指针叫虚类指针,也是四个字节;还要包括类a的字节数,所以类b的字节数就是12。

参考:

菱形继承和虚继承
关于C++中的虚拟继承的一些总结

下标运算符:[]

MSDN定义如下:

1
postfix-expression [ expression ]

通常,postfix-expression 表示的值是一个指针值(如数组标识符),xpression 是一个整数值(包括枚举类型)。 但是,从语法上来说,只需要一个表达式是指针类型,另一个表达式是整型。 因此整数值可以位于 postfix-expression 位置,指针值可以位于 expression 的方括号中或下标位置。

表达式 nArray[2]2[nArray] 相同。 原因是下标表达式 e1[ e2 ] 的结果由以下所示给定:

1
*( ( e2 ) + (e1) )

该表达式生成的地址不是 e1 地址中的 e2 字节。 相反,该地址将进行缩放以生成数组 e2 中的下一个对象。

boost 智能指针

boost库中的智能指针主要有:

  • boost::scoped_ptr<T>
  • boost::shared_ptr<T>
  • boost::weak_ptr<T>

###boost::scoped_ptr<T>

解释:

当这个指针的作用域结束之后自动释放,与auto_ptr不同之处在于,所有权不能转移,但是可以交换。

boost::scoped_ptr的实现和std::auto_ptr非常类似,都是利用了一个栈上的对象去管理一个堆上的对象,从而使得堆上的对象随着栈上的对象销毁时自动删除。

特点:

  • 不能转换所有权
    boost::scoped_ptr所管理的对象生命周期仅仅局限于一个区间(该指针所在的”{}”之间),无法传到区间之外,这就意味着boost::scoped_ptr对象是不能作为函数的返回值的(std::auto_ptr可以)。

  • 不能共享所有权
    这点和std::auto_ptr类似。这个特点一方面使得该指针简单易用。另一方面也造成了功能的薄弱——不能用于stl的容器中。

  • 不能用于管理数组对象
    由于boost::scoped_ptr是通过delete来删除所管理对象的,而数组对象必须通过deletep[]来删除,因此boost::scoped_ptr是不能管理数组对象的,如果要管理数组对象需要使用boost::scoped_array类。

选取:

boost::scoped_ptrstd::auto_ptr的功能和操作都非常类似,如何在他们之间选取取决于是否需要转移所管理的对象的所有权(如是否需要作为函数的返回值)。如果没有这个需要的话,大可以使用boost::scoped_ptr,让编译器来进行更严格的检查,来发现一些不正确的赋值操作。

使用:

1
2
3
4
boost::scoped_ptr<int> sp ( new int );
int * lp2 = new int;
boost::scoped_ptr<int> sp2 ( lp2 );
sp.swap(sp2);

参考:

Boost智能指针——scoped_ptr

###boost::shared_ptr<T>

解释:

内部维护一个引用计数器来判断此指针是不是需要被释放。

和前面介绍的boost::scoped_ptr相比,boost::shared_ptr可以共享对象的所有权,因此其使用范围基本上没有什么限制(不是绝对安全),自然也可以使用在stl的容器中。另外它还是线程安全的,这点在多线程程序中也非常重要。

使用:

  • 避免对shared_ptr所管理的对象的直接内存管理操作,以免造成该对象的重释放

  • shared_ptr并不能对循环引用的对象内存自动管理(这点是其它各种引用计数管理内存方式的通病)。

  • 不要构造一个临时的shared_ptr作为函数的参数。

1
2
3
4
5
6
7
8
boost::shared_ptr<int> sp1(new int);
//sp1.use_count() = 1
boost::shared_ptr<int> sp2 = sp1;
//sp2.use_count() = 2
sp1.reset();
//sp2.use_count() = 1
sp2.reset();
//引用计数变为0,对象被自动删除

内存泄露例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
void f(shared_ptr<int>, int);
int g();

void ok()
{
shared_ptr<int> p(new int(2));
f(p, g());
}

void bad()
{
f(shared_ptr<int>(new int(2)), g());
}

bad 函数内,假设先构造了堆对象,接着执行g(), 在g()函数内抛出了异常,那么由于裸指针还没有被智能指针接管,就会出现内存泄漏。

###boost::weak_ptr<T>

解释:

弱指针,它不控制对象的生命期,但是它知道对象是否还活着。如果还活着,那么它可以提升(promote)为有效的shared_ptr;如果对象已经死了,提升会失败,返回一个空的shared_ptr

弱引用当引用的对象活着的时候不一定存在。仅仅是当它存在的时候的一个引用。弱引用并不修改该对象的引用计数,这意味这弱引用它并不对对象的内存进行管理,在功能上类似于普通指针,然而一个比较大的区别是,弱引用能检测到所管理的对象是否已经被释放,从而避免访问非法内存。

boost::weak_ptr除了对所管理对象的基本访问功能(通过get()函数)外,还有两个常用的功能函数:

  • expired()用于检测所管理的对象是否已经释放;
  • lock()用于获取所管理的对象的强引用指针。

参考:

Boost智能指针——weak_ptr

###std::unique_ptr

unique_ptr的“唯一”拥有其所指对象,同一时刻只能有一个unique_ptr指向给定对象(通过禁止拷贝语义、只有移动语义来实现)。

unique_ptr指针本身的生命周期:从unique_ptr指针创建时开始,直到离开作用域。离开作用域时,若其指向对象,则将其所指对象销毁(默认使用delete操作符,用户可指定其他操作)。

unique_ptr指针与其所指对象的关系:在智能指针生命周期内,可以改变智能指针所指对象,如创建智能指针时通过构造函数指定、通过reset方法重新指定、通过release方法释放所有权、通过移动语义转移所有权。

  • 动态资源的异常安全保证

    1
    2
    3
    4
    5
    6
    void foo()
    {
    //异常安全的代码。无论是否异常发生,只要px指针成功创建,其析构函数都会被调用,确保动态资源被释放
    unique_ptr<X> px(new X);
    // do something,
    }
  • 返回函数内创建的动态资源

    1
    2
    3
    4
    5
    6
    unique_ptr<X> foo()
    {
    unique_ptr<X> px(new X);
    // do something
    return px; //移动语义
    }
  • 可放在容器中(弥补了auto_ptr不能作为容器元素的缺点)

    1
    2
    3
    vector<unique_ptr<string>>v;
    unique_ptr<string> p1(new string("abc"));
    v.push_back(std::move(p1));//这里需要显式的移动语义,因为unique_ptr并无copy语义

auto_ptrunique_ptr

C++11环境下,auto_ptr被看做“遗留的”,他们有如下区别:

  • auto_ptr有拷贝语义,拷贝后源对象变得无效;unique_ptr则无拷贝语义,但提供了移动语义

  • auto_ptr不可作为容器元素,unique_ptr可以作为容器元素

  • auto_ptr不可指向动态数组(尽管不会报错,但不会表现出正确行为),unique_ptr可以指向动态数组

###补充

在c++11标准库中引入了,std::shared_ptrstd::weak_ptr,在用法上和boost库区别不大。可以看看下面的文章。

C++ 智能指针类
Should I switch from using boost::shared_ptr to std::shared_ptr?

###参考

从零开始学C++之boost库(一):详解 boost 库智能指针(scoped_ptr 、shared_ptr 、weak_ptr 源码分析)
C++11智能指针之unique_ptr

不可以被声明为虚函数

常见的不能声明为虚函数的有:普通函数(非成员函数);静态成员函数内联成员函数构造函数友元函数

  • 不支持普通函数为虚函数

    普通函数(非成员函数)只能被overload,不能被override,声明为虚函数也没有什么意思,因此编译器会在编译时邦定函数。

  • 不支持构造函数为虚函数

    主要是从语义上考虑,所以不支持。因为构造函数本来就是为了明确初始化对象成员才产生的,然而virtual function主要是为了在不完全了解细节的情况下也能正确处理对象。另外,virtual函数是在不同类型的对象产生不同的动作,现在对象还没有产生,如何使用virtual函数来完成你想完成的动作。(这不就是典型的悖论)

  • 不支持内联成员函数为虚函数

    内联函数就是为了在代码中直接展开,减少函数调用花费的代价,虚函数是为了在继承后对象能够准确的执行自己的动作,这是不可能统一的。(再说了,inline函数在编译时被展开虚函数在运行时才能动态的绑定函数

  • 不支持静态成员函数为虚函数

    静态成员函数对于每个类来说只有一份代码,所有的对象都共享这一份代码,没有要动态邦定的必要性。

  • 不支持友元函数为虚函数

    因为C++不支持友元函数的继承,对于没有继承特性的函数没有虚函数的说法。

不可被重载的运算符

  • . —- 成员访问运算符

  • .* —- 指向成员操作的指针

  • :: —- 域运算符

  • ? : —- 条件运算符

  • sizeof —- 对象大小运算符

  • typeid —- 返回标准库类型的对象的引用

4种类型转换cast

  • reinterpret_cast

    reinterpret_cast转换一个指针为其它类型的指针,也允许将任何整数类型转换为任何指针类型以及反向转换。

    通常为操作数的位模式提供较低层次的重新解释。

    这个操作符能够在非相关的类型之间转换。操作结果只是简单的从一个指针到别的指针的值的二进制拷贝。在类型之间指向的内容不做任何类型的检查和转换。

    1
    2
    3
    4
    class A {};
    class B {};
    A * a = new A;
    B * b = reinterpret_cast<B *>(a);

    reinterpret_cast就像传统的类型转换一样对待所有指针的类型转换。

  • static_cast

    static_cast允许执行任意的隐式转换和相反转换动作。

    应用到类的指针上,意思是说它允许子类类型的指针转换为父类类型的指针(这是一个有效的隐式转换),同时,也能够执行相反动作:转换父类为它的子类。

    1
    2
    3
    4
    class Base {};
    class Derived : public Base {};
    Base *a = new Base;
    Derived *b = static_cast<Derived *>(a);

    static_cast除了操作类型指针,也能用于执行类型定义的显式的转换,以及基础类型之间的标准转换。

    1
    2
    double d = 3.14159265;
    int i = static_cast<int>(d);
  • dynamic_cast

    dynamic_cast支持运行时识别指针或引用所指的对象,将基类类型对象的引用或指针转换为同一继承层次中其他类型的引用或指针。当用于多态类型时,它允许任意的隐式类型转换以及相反过程。

    dynamic_cast一起使用的指针必须是有有效的,必须为0或者指向一个对象。

    不过,与static_cast不同,在后一种情况里(注:即隐式转换的相反过程),dynamic_cast会检查操作是否有效。也就是说,它会检查转换是否会返回一个被请求的有效的完整对象。

    检测在运行时进行。如果被转换的指针不是一个被请求的有效完整的对象指针,返回值为NULL。

    1
    2
    3
    4
    5
    6
    class Base { virtual dummy() {} };
    class Derived : public Base {};
    Base* b1 = new Derived;
    Base* b2 = new Base;
    Derived* d1 = dynamic_cast<Derived *>(b1); // succeeds
    Derived* d2 = dynamic_cast<Derived *>(b2); // fails: returns 'NULL'

    如果一个引用类型执行了类型转换并且这个转换是不可能的,一个bad_cast的异常类型被抛出:

    1
    2
    3
    4
    5
    6
    7
    8
    void f(const Base &b)
    {
    try{
    const Derived &d = dynamic_cast<const Derived&>(b);
    }catch (bad_cast){
    //handle the fact that the cast failed
    }
    }
  • const_cast

    这个转换类型操纵传递对象的const属性,或者是设置或者是移除:
    const_cast中的类型必须是指针、引用或指向对象类型成员的指针。

    1
    2
    3
    class C {};
    const C *a = new C;
    C *b = const_cast<C *>(a);

typeid操作符

typeid操作符使程序能够为一个表达式:你是什么类型?

头文件: typeinfo

1
typeid(e)

typeid操作符的结果是名为type_info的标准库类型的对象引用。

1
2
3
4
5
6
Base *bd;
Derived *dp;
if(typeid(*bp) == typeid(*dp)){
}
if(typeid(*bp) == typeid(Derived)){
}

typeid的操作数是表示对象的表达式—-测试*bp,而不是bp。

以下测试将Base*类型与Derived类型比较,总是失败:

1
2
if(typeid(bp) == typeid(Derived)){
}