C++知识点

智能指针

4种智能指针

STL一共给我们提供了四种智能指针:auto_ptr、unique_ptr、shared_ptr和weak_ptr。

  1. auto_ptr不常用,因为其不仅不符合 C++ 编程思想,而且极容易出错。
  2. unique_ptr不允许你对它进行拷贝构造和赋值,它将赋值重载和拷贝构造两个函数只进行了声明而没有实现,这样它就强制限定你不可能在使用其他指针访问这块空间。
  3. shared_ptr是比较流行和实用的智能指针了,它通过计数器原理解决了上述两种智能指针访问唯一性的问题,它允许多个指针访问同一块空间,并且在析构时也能够保证内存正确释放。
  4. weak_ptr是shared_ptr的拓展,它可以解决后者的循环引用问题。

手动实现引用计数的智能指针

下面是一个基于引用计数的智能指针的实现,需要实现构造,析构,拷贝构造,=操作符重载,重载*和->操作符。

注意到这里的引用计数也是一个指针。

template <typename T>
class SmartPointer {
public:
    //构造函数
    SmartPointer(T* p=0): _ptr(p), _reference_count(new size_t)
    {
        if(p)
            *_reference_count = 1; 
        else
            *_reference_count = 0; 
    }
    //拷贝构造函数
    SmartPointer(const SmartPointer& src) {
        if(this!=&src) 
        {
            _ptr = src._ptr;
            _reference_count = src._reference_count;
            (*_reference_count)++;
        }
    }
    //重载赋值操作符
    SmartPointer& operator=(const SmartPointer& src) {
        if(_ptr==src._ptr) {
            return *this;
        }
        //这里调用releaseCount,是因为原来的_ptr即将被覆盖,所以原来的元素的引用要-1
        releaseCount();
        _ptr = src._ptr;
        _reference_count = src._reference_count;
        (*_reference_count)++;
        return *this;
    }

    //重载操作符
    T& operator*() {
        if(_ptr)
            return *_ptr;
        //throw exception
    }
    //重载操作符
    T* operator->() {
        if(_ptr)
            return _ptr;
        //throw exception
    }
    //析构函数
    ~SmartPointer() {
        if (--(*_reference_count) == 0) 
        {
            delete _ptr;
            delete _reference_count;
        }
    }
private:
    T* _ptr;
    size_t* _reference_count;
    void releaseCount() 
    {
        if(_ptr) 
        {
            (*_reference_count)--;
            if((*_reference_count)==0) 
            {
                delete _ptr;
                delete _reference_count;
            }
        }
    }
};

int main() 
{
    SmartPointer<char> cp1(new char('a'));
    SmartPointer<char> cp2(cp1);
    SmartPointer<char> cp3;
    cp3 = cp2;
    cp3 = cp1;
    cp3 = cp3;
    SmartPointer<char> cp4(new char('b'));
    cp3 = cp4;
}

拷贝构造函数和赋值构造函数

拷贝构造函数

拷贝构造是确确实实构造一个新的对象,并给新对象的私有成员赋上参数对象的私有成员的值,新构造的对象和参数对象地址是不一样的,所以如果该类中有一个私有成员是指向堆中某一块内存,如果仅仅对该私有成员进行浅拷贝,那么会出现多个指针指向堆中同一块内存,这是会出现问题,如果那块内存被释放了,就会出现其他指针指向一块被释放的内存,出现未定义的值的问题,如果深拷贝,就不会出现问题,因为深拷贝,不会出现指向堆中同一块内存的问题,因为每一次拷贝,都会开辟新的内存供对象存放其值。
拷贝构造函数没有返回类型。

浅拷贝

class A {
private:
    int* n;
public:
    A() {
        n = new int[10];
        n[0] = 1;
        cout<<"constructor is called\n";
    }

    A(const A& a) {
        n = a.n;
        cout<<"copy constructor is called\n";
    }

    ~A() {
        cout<<"destructor is called\n";
        delete n;
    }

    void get() {
        cout<<"n[0]: "<<n[0]<<endl;
    }
};

int main() {
    A* a = new A();
    A b = *a;
    delete a;
    b.get();
    return 0;
}

由于a和b的n都指向了同一个地址,所以如果把a删除了,那b也会出错。

深拷贝

......
    A(const A& a) {
        n = new int[10];
        memcpy(n, a.n, 10);  //通过按字节拷贝,将堆中一块内存存储到另一块内存
        cout<<"copy constructor is called\n";
    }
......

这样就不会出错。

赋值构造函数

赋值构造函数是将一个参数对象中私有成员赋给一个已经在内存中占据内存的对象的私有成员,赋值构造函数被赋值的对象必须已经被创建了,否则调用的将是拷贝构造函数,当然赋值构造函数也有深拷贝和浅拷贝的问题。
赋值构造函数必须能够处理自我赋值的问题,因为自我赋值会出现指针指向一个已经释放的内存。还有赋值构造函数必须注意它的函数原型,参数必须是引用类型,返回值也必须是引用类型,否则在传参和返回的时候都会再次调用一次拷贝构造函数。

深拷贝

......
    A& operator=(const A& a)  //记住形参和返回值一定要是引用类型,否则传参和返回时会自动调用拷贝构造函数
    {
        if(this == &a)     //为什么需要进行自我赋值判断呢?因为下面要进行释放n的操作,如果是自我赋值,而没有进行判断的话,那么就会出现讲一个释放了的内存赋给一个指针
            return *this;
        if(n != NULL) {
            delete n;
            n == NULL;   //记住释放完内存将指针赋为NULL
        }

        n = new int[10];
        memcpy(n, a.n, 10);
        cout<<"assign constructor is called\n";
        return *this;
    }

单例模式

懒汉式

懒汉式的特点是延迟加载,采用懒汉式的方法,顾名思义,懒汉么,很懒的,配置文件的实例直到用到的时候才会加载。

class CSingleton  
{  
public:  
static CSingleton* GetInstance()  
{  
     if ( m_pInstance == NULL )    
         m_pInstance = new CSingleton();  
     return m_pInstance;  
}  
private:  
    CSingleton(){};  
    static CSingleton * m_pInstance;  
};  

在懒汉式的单例类中,其实有两个状态,单例未初始化和单例已经初始化。假设单例还未初始化,有两个线程同时调用GetInstance方法,这时执行 m_pInstance == NULL 肯定为真,然后两个线程都初始化一个单例,最后得到的指针并不是指向同一个地方,不满足单例类的定义了。
所以懒汉式单例不是线程安全的。

饿汉式

饿汉式的特点是一开始就加载了,如果说懒汉式是“时间换空间”,那么饿汉式就是“空间换时间”,因为一开始就创建了实例,所以每次用到的之后直接返回就好了。

class Singleton  
{  
private:  
    static const Singleton* pInstance;  
    Singleton(){}  
public:  
    static const Singleton* getInstace()  
    {  
        return pInstance;  
    }  
};  

// 外部初始化(进入主函数之前)  
const Singleton* Singleton::pInstance = new Singleton;  

饿汉式是线程安全的,在类创建的同时就已经创建好一个静态的对象供系统使用,以后不再改变。

线程安全的懒汉模式

class Singleton  
{  
private:  
    static Singleton* m_instance;  
    Singleton(){}  
public:  
    static Singleton* getInstance()
    {  
        if(NULL == m_instance)  
        {  
            Lock();//借用其它类来实现,如boost  
            if(NULL == m_instance)  
            {  
                m_instance = new Singleton;  
            }  
            UnLock();  
        }  
        return m_instance;  
    };  
};

这里用到了锁,并且进行了double-check。

C++的内存分配

内存可以基本分为3大部分:静态存储区、堆区和栈区。

静态存储区:内存在程序编译的时候就已经分配好,这块内存在程序的整个运行期间都存在。它主要存放静态变量、全局变量和常量。

栈区:在执行函数时,函数内的局部变量在栈上创建,函数执行结束时,这些局部变量被自动释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。

堆区:亦称动态内存分配。程序在运行的时候用malloc或new申请任意大小的内存,程序员自己负责在适当的时候用free或delete释放内存。
动态内存的生存期可以由我们决定,如果我们不释放内存,程序将在最后才释放掉动态内存。 但是良好的编程习惯是:如果某动态内存不再使用,需要将其释放掉,否则,我们认为发生了内存泄漏现象。

三者最大的不同在于:栈的生命周期很短暂,而堆区和静态存储区的生命周期比较长。
堆区的内存空间使用更加灵活,因为它允许你在不需要它的时候,随时将它释放掉,而静态存储区将一直存在于程序的整个生命周期中。

C++对象的内存布局

在C++中,有两种类的成员变量:static和非static,有三种成员函数:static、非static和virtual。那么,它们如何影响C++的对象在内存中的分布呢? 当存在继承的情况下,其内存分布又是如何呢?

源文件转换为可执行文件

源文件经过以下几步生成可执行文件:

  • 预处理(preprocessor):对#include、#define、#ifdef/#endif、#ifndef/#endif等进行处理
  • 编译(compiler):将源码编译为汇编代码
  • 汇编(assembler):将汇编代码汇编为目标代码
  • 链接(linker):将目标代码链接为可执行文件
    这里写图片描述

可执行文件组成及内存布局

典型的可执行文件分为两部分:

代码段(Code),由机器指令组成,该部分是不可改的,编译之后就不再改变,放置在文本段(.text)。
数据段(Data),它由以下几部分组成:

  • 常量(constant),通常放置在只读read-only的文本段(.text)
  • 静态数据(static data),初始化的放置在数据段(.data);未初始化的放置在(.bss,Block Started by Symbol,BSS段的变量只有名称和大小而没有值)
  • 动态数据(dynamic data),存储在堆(heap)或栈(stack)

源程序编译后链接到一个以0地址为始地址的线性或多维虚拟地址空间。而且每个进程都拥有这样一个空间,每个指令和数据都在这个虚拟地址空间拥有确定的地址,把这个地址称为虚拟地址(Virtual Address)。
将进程中的目标代码、数据等的虚拟地址组成的虚拟空间称为虚拟存储器(Virtual Memory)。典型的虚拟存储器中有类似的布局:

Text Segment (.text)
Initialized Data Segment (.data)
Uninitialized Data Segment (.bss)
The Stack
The Heap
如下图所示:
这里写图片描述

数据存储类别

讨论C/C++中的内存布局,不得不提的是数据的存储类别!数据在内存中的位置取决于它的存储类别。一个对象是内存的一个位置,解析这个对象依赖于两个属性:存储类别、数据类型。

  • 存储类别决定对象在内存中的生命周期。
  • 数据类型决定对象值的意义,在内存中占多大空间。

C/C++中由(auto、 extern、 register、 static)存储类别和对象声明的上下文决定它的存储类别。

自动对象(automatic objects)

auto和register将声明的对象指定为自动存储类别。他们的作用域是局部的,诸如一个函数内,一个代码块内等。操作了作用域,对象会被销毁。

在一个代码块中声明一个对象,如果没有执行auto,那么默认是自动存储类别。
声明为register的对象是自动存储类别,存储在计算机的快速寄存器中。不可以对register对象做取值操作“&”。

静态对象(static objects)

静态对象可以局部的,也可以是全局的。静态对象一直保持它的值,例如进入一个函数,函数中的静态对象仍保持上次调用时的值。包含静态对象的函数不是线程安全的、不可重入的,正是因为它具有“记忆”功能。

局部对象声明为静态之后,将改变它在内存中保存的位置,由动态数据—>静态数据,即从堆或栈变为数据段或bbs段。
全局对象声明为静态之后,而不会改变它在内存中保存的位置,仍然是在数据段或bbs段。但是static将改变它的作用域,即该对象仅在本源文件有效
与此相反的关键字是extern,使用extern修饰或者什么都不带的全局对象的作用域是整个程序。

含有非static成员变量及成员函数的类的对象的内存分布

类Persion的定义如下:

class Person
{
    public:
        Person():mId(0), mAge(20){}
        void print()
        {
            cout << "id: " << mId
                 << ", age: " << mAge << endl;
        }
    private:
        int mId;
        int mAge;
}; 

Person类包含两个非static的int型的成员变量,一个构造函数和一个非static成员函数。
非static成员变量被放置于每一个类对象中,非static成员函数放在类的对象之外,且非static成员变量在内存中的存放顺序与其在类内的声明顺序一致。即person对象的内存分布如下图所示:
这里写图片描述

含有static和非static成员变量和成员函数的类的对象的内存分布

向Person类中加入一个static成员变量和一个static成员函数,如下:

class Person
{
     public:
         Person():mId(0), mAge(20){ ++sCount; }
         ~Person(){ --sCount; }
         void print()
         {
             cout << "id: " << mId
                  << ", age: " << mAge << endl;
         }
         static int personCount()
         {
             return sCount;
         }
     private:
         static int sCount;
         int mId;
         int mAge;
}; 

可以得出:static成员变量存放在类的对象之外,static成员函数也放在类的对象之外。
这里写图片描述

加入virtual成员函数的类的对象的内存分布

在Person类中加入一个虚函数,并把前面的print函数也修改为虚函数,如下:

class Person
{
    public:
        Person():mId(0), mAge(20){ ++sCount; }
        static int personCount()
        {
            return sCount;
        }

        virtual void print()
        {
            cout << "id: " << mId
                 << ", age: " << mAge << endl;
        }
        virtual void job()
        {
            cout << "Person" << endl;
        }
        virtual ~Person()
        {
            --sCount;
            cout << "~Person" << endl;
        }

    protected:
        static int sCount;
        int mId;
        int mAge;
};

加virtual成员函数后,类的对象的大小为16字节,增加了8。通过int*指针遍历该对象的内存,可以看到,最后两行显示的是成员数据的值。

C++中的虚函数是通过虚函数表(vtbl)来实现,每一个类为每一个virtual函数产生一个指针,放在表格中,这个表格就是虚函数表。每一个类对象会被安插一个指针(vptr),指向该类的虚函数表。vptr的设定和重置都由每一个类的构造函数、析构函数和复制赋值运算符自动完成。

由于本人的系统是64位的系统,一个指针的大小为8字节,所以可以推出,在本人的环境中,类的对象的安插的vptr放在该对象所占内存的最前面。其内存分布图如下:
注:虚函数的顺序是按虚函数定义顺序定义的,但是它还包含其他的一些字段,本人还未明白它是什么,在下一节会详细说明虚函数表的内容。
这里写图片描述

C++11新特性

lambda表达式(匿名函数)

Lambda表达式具体形式如下:

[capture](parameters)->return-type{body}

具体 

auto

在C++11中,auto的功能变为类型推断。auto现在成了一个类型的占位符,通知编译器去根据初始化代码推断所声明变量的真实类型。各种作用域内声明变量都可以用到它。例如,名空间中,程序块中,或是for循环的初始化语句中。

auto i = 42;        // i is an int
auto l = 42LL;      // l is an long long
auto p = new foo(); // p is a foo*

使用auto通常意味着更短的代码(除非你所用类型是int,它会比auto少一个字母)。试想一下当你遍历STL容器时需要声明的那些迭代器(iterator)。现在不需要去声明那些typedef就可以得到简洁的代码了。

std::map<std::string, std::vector<int>> map;
for(auto it = begin(map); it != end(map); ++it) 
    ...

for_each循环

为了在遍历容器时支持”for each”用法,C++11扩展了for语句的语法。用这个新的写法,可以遍历C类型的数组、初始化列表以及任何重载了非成员的begin()和end()函数的类型。
如果你只是想对集合或数组的每个元素做一些操作,而不关心下标、迭代器位置或者元素个数,那么这种for each的for循环将会非常有用。

结合Lambda表达式和for_each

    int a[] = {1,2,5,9,52,6,3,14};
    function <bool (const int & , const int &)> compare;
    // C++11标准中, auto 关键字新义,任意类型,类型由初始化表达式确定
    // “[](int n)->void{ cout << n << endl; }”是Lambda表达式
    auto output = [](int n)->void{ cout << n << endl; };

    cout << "升序排序" << endl;
    compare = []( const int & a , const int & b )->bool { return a < b; };
    sort( a, a+_countof(a), compare  );
    for_each( a, a+_countof(a), output );
    cout << endl;

非成员begin()和end()

他们是新加入标准库的,除了能提高了代码一致性,还有助于更多地使用泛型编程。它们和所有的STL容器兼容。更重要的是,他们是可重载的。所以它们可以被扩展到支持任何类型。对C类型数组的重载已经包含在标准库中了。

hash_map和map

区别

构造函数。hash_map需要hash函数,等于函数;map只需要比较函数(小于函数).
存储结构。hash_map采用hash表存储,map一般采用红黑树(RB Tree)实现。因此其memory数据结构是不一样的。

使用场景

总体来说,hash_map 查找速度会比map快,而且查找速度基本和数据数据量大小,属于常数级别;而map的查找速度是log(n)级别。
但是,并不一定常数就比log(n)小,hash还有hash函数的耗时,如果你考虑效率,特别是在元素达到一定数量级时,考虑考虑hash_map。但若你对内存使用特别严格,希望程序尽可能少消耗内存,那么一定要小心,hash_map可能会让你陷入尴尬,特别是当你的hash_map对象特别多时,你就更无法控制了,而且hash_map的构造速度较慢。

现在知道如何选择了吗?权衡三个因素: 查找速度, 数据量, 内存使用。

如何在hash_map中加入自己定义的类型?

你只要做两件事, 定义hash函数,定义等于比较函数。下面的代码是一个例子:

#include <hash_map>
#include <string>
#include <iostream>

using namespace std;
//define the class
class ClassA{
        public:
        ClassA(int a):c_a(a){}
        int getvalue()const { return c_a;}
        void setvalue(int a){c_a;}
        private:
        int c_a;
};

//1 define the hash function
struct hash_A {
        size_t operator()(const class ClassA & A)const{
                //  return  hash<int>(classA.getvalue());
                return A.getvalue();
        }
};

//2 define the equal function
struct equal_A {
        bool operator()(const class ClassA & a1, const class ClassA & a2)const{
                return  a1.getvalue() == a2.getvalue();
        }
};

int main() {
        hash_map<ClassA, string, hash_A, equal_A> hmap;
        ClassA a1(12);
        hmap[a1]="I am 12";
        ClassA a2(198877);
        hmap[a2]="I am 198877";

        cout<<hmap[a1]<<endl;
        cout<<hmap[a2]<<endl;
        return 0;
}

I am 12
I am 198877

static 用途

static 局部变量:表示该变量不是auto型的,就是说,该变量在程序开始的时候创建,在程序结束的时候存储空间不释放,使用的时候沿用上一次的那个值。
static 全局变量:表示该变量只能在本文件中使用,不能被其他文件使用。
static 函数:表示该函数只能在本文件中使用,不能被其他文件中的函数调用。
static 类成员变量:表示这个类被全类拥有,该类的所有对象只有一份拷贝。
static 成员函数:表示这个函数被全类拥有,不接收this指针,所以只能访问静态成员变量。

const用途

const 常量:该数不允许被改变,若改变编译器报错。定义时就必须要初始化。
const 函数形参:f(const int a);表示该形参是一个输入形参,在函数里不能改变其值。
const 指针:让指针本身为const 或者指针所指为const 或者两者同时为const。
类的const 成员函数 :表示该函数只能对成员变量进行只读操作。
const 类的成员函数:返回值为const,使得其返回值不能左值。

const修饰类的成员函数

类的成员函数后面加 const,表明这个函数不会对这个类对象的数据成员(准确地说是非静态数据成员)作任何改变。

在设计类的时候,一个原则就是对于不改变数据成员的成员函数都要在后面加 const,而对于改变数据成员的成员函数不能加 const。所以 const 关键字对成员函数的行为作了更加明确的限定:有 const 修饰的成员函数(指 const 放在函数参数表的后面,而不是在函数前面或者参数表内),只能读取数据成员,不能改变数据成员;没有 const 修饰的成员函数,对数据成员则是可读可写的。

除此之外,在类的成员函数后面加 const 还有什么好处呢?那就是常量(即 const)对象可以调用 const 成员函数,而不能调用非const修饰的函数。

例如:
注意:两个成员函数如果只是常量性不同,是可以被重载的。

class A {
public:
    void f() {
        cout<<"non const"<<endl;
     } 
    void f() const
    {
        cout<<" const"<<endl;
     } 
};

下面,const 对象调用f() const,非const对象调用 f()。

const 如何实现?

如果被修饰的常量是基本类型,那么程序在编译时,就将变量用常量来替换了:
实现机制:这些在编译期间完成,对于内置类型,如int,编译器可能使用常数直接替换掉对此变量的引用。
如果const 变量修饰的是非基本类型,那么程序编译时,不知道该用什么值替换再编译;所以,将会用一块内存地址替换,然后再编译。

虚函数的实现

简单地说,每一个含有虚函数(无论是其本身的,还是继承而来的)的类都至少有一个与之对应的虚函数表,其中存放着该类所有的虚函数对应的函数指针。
虚函数表放在内存中,它存放了这个类的虚函数指针。而每个类的对象都有一个虚表指针vptr指向它。

例:
这里写图片描述
其中:
B的虚函数表中存放着B::foo和B::bar两个函数指针。
D的虚函数表中存放的既有继承自B的虚函数B::foo,又有重写(override)了基类虚函数B::bar的D::bar,还有新增的虚函数D::quz。

虚函数表的构造过程

从编译器的角度来说,B的虚函数表很好构造,D的虚函数表构造过程相对复杂。下面给出了构造D的虚函数表的一种方式:

这里写图片描述

虚函数调用过程

以下面的程序为例:
这里写图片描述

编译器只知道pb是B*类型的指针,并不知道它指向的具体对象类型 :pb可能指向的是B的对象,也可能指向的是D的对象。

但对于“pb->bar()”,编译时能够确定的是:此处operator->的另一个参数是B::bar(因为pb是B*类型的,编译器认为bar是B::bar),而B::bar和D::bar在各自虚函数表中的偏移位置是相等的。

无论pb指向哪种类型的对象,只要能够确定被调函数在虚函数中的偏移值,待运行时,能够确定具体类型,并能找到相应vptr了,就能找出真正应该调用的函数。

虚函数指针中的ptr部分为虚函数表中的偏移值(以字节为单位)加1。B::bar是一个虚函数指针, 它的ptr部分内容为9,它在B的虚函数表中的偏移值为8(8+1=9)。

当程序执行到“pb->bar()”时,已经能够判断pb指向的具体类型了:如果pb指向B的对象,可以获取到B对象的vptr,加上偏移值8((char*)vptr + 8),可以找到B::bar。如果pb指向D的对象,可以获取到D对象的vptr,加上偏移值8((char*)vptr + 8) ,可以找到D::bar。
如果pb指向其它类型对象…同理…

多重继承和虚继承

当一个类继承多个类,且多个基类都有虚函数时,子类对象中将包含多个虚函数表的指针(即多个vptr),例:
这里写图片描述

其中:D自身的虚函数与B基类共用了同一个虚函数表,因此也称B为D的主基类(primary base class)。
虚函数替换过程与前面描述类似,只是多了一个虚函数表,多了一次拷贝和替换的过程。
虚函数的调用过程,与前面描述基本类似,区别在于基类指针指向的位置可能不是派生类对象的起始位置,以如下面的程序为例:
这里写图片描述

虚继承就是为了节约内存的,它是多重继承中的特有的概念。适用于菱形继承形式。
如:类B、C都继承类A,D继承类B和C。为了节省内存空间,可以将B、C对A的继承定义为虚拟继承,此时A就成了虚拟基类。
class A;
class B:vitual public A;
class C:vitual public A;
class D:public B,public C;

菱形继承和虚继承

这里写图片描述
B和C从A中继承,而D多重继承于B,C。那就意味着D中会有A中的两个拷贝。因为成员函数不体现在类的内存大小上,所以实际上可以看到的情况是D的内存分布中含有2组A的成员变量。如下代码:

class A  
{  
public:  
    A():a(1){};  
    void printA(){cout<<a<<endl;}  
    int a;  
};  

class B : public A  
{  
};  

class C : public A  
{  
};  

class D:  public B ,  public C  
{  
};  

int _tmain(int argc, _TCHAR* argv[])  
{  
    D d;  
    cout<<sizeof(d);  
}  

输出d的大小为8。也就是d中有2个a成员变量。这样一来如果要使用a就会出现“二义性”这个问题。

这时候虚继承就出场了。虚继承是一种机制,类通过虚继承指出它希望共享虚基类的状态。对给定的虚基类,无论该类在派生层次中作为虚基类出现多少次,只继承一个共享的基类子对象,共享基类子对象称为虚基类。虚基类用virtual声明继承关系就行了。这样一来,D就只有A的一份拷贝。如下:

class A  
{  
public:  
    A():a(1){};  
    void printA(){cout<<a<<endl;}  
    int a;  
};  

class B : virtual public A  
{  
};  

class C : virtual public A  
{  
};  

class D:  public B ,  public C  
{  
};  

int _tmain(int argc, _TCHAR* argv[])  
{  
    D d;  
    cout<<sizeof(d);  
    d.a=10;  
    d.printA();  
}  

输出d的大小是12(包含了2个4字节的D类虚基指针和1个4字节的int型整数)。而a和printA()都可以正常访问。

malloc/free与new/delete的区别

相同点:都可用于申请动态内存和释放内存。
不同点:

操作对象不同

malloc与free是C++/C 语言的标准库函数,new/delete 是C++的运算符。对于非内部数据类的对象而言,光用maloc/free 无法满足动态对象的要求。对象在创建的同时要自动执行构造函数, 对象消亡之前要自动执行析构函数。由于malloc/free 是库函数而不是运算符,不在编译器控制权限之内,不能够把执行构造函数和析构函数的任务强加malloc/free。

用法不同

malloc和free

函数malloc 的原型如下:

void * malloc(size_t size);

用malloc 申请一块长度为length 的整数类型的内存,程序如下:

int *p = (int *) malloc(sizeof(int) * length);

我们应当把注意力集中在两个要素上:“类型转换”和“sizeof”。
1、malloc 返回值的类型是void ,所以在调用malloc 时要显式地进行类型转换,将void 转换成所需要的指针类型。
2、 malloc 函数本身并不识别要申请的内存是什么类型,它只关心内存的总字节数。

函数free 的原型如下:

void free( void * memblock );

为什么free 函数不象malloc 函数那样复杂呢?这是因为指针p 的类型以及它所指的内存的容量事先都是知道的,语句free(p)能正确地释放内存。如果p 是NULL 指针,那么free 对p 无论操作多少次都不会出问题。如果p 不是NULL 指针,那么free 对p连续操作两次就会导致程序运行错误。

new和delete

运算符new 使用起来要比函数malloc 简单得多,例如:

int *p1 = (int *)malloc(sizeof(int) * length);
int *p2 = new int[length];

这是因为new 内置了sizeof、类型转换和类型安全检查功能。对于非内部数据类型的对象而言,new 在创建动态对象的同时完成了初始化工作。如果对象有多个构造函数,那么new 的语句也可以有多种形式。
如果用new 创建对象数组,那么只能使用对象的无参数构造函数。例如

Obj *objects = new Obj[100];       // 创建100 个动态对象
不能写成
Obj *objects = new Obj[100](1);        // 创建100 个动态对象的同时赋初值1

在用delete 释放对象数组时,留意不要丢了符号‘[]’。例如

delete []objects; // 正确的用法
delete objects; // 错误的用法

后者相当于delete objects[0],漏掉了另外99 个对象。

用delete释放指针所指向的对象十分方便,直接

Node* p = new Node();
...
delete p;
p = NULL;

就行了,但是注意,delete只是释放了p所指向对象的内存,但p如果不赋NULL的话,就会变成野指针了。所以一般要在delete后面紧跟一个赋NULL的操作。

本质区别

malloc/free是C/C++语言的标准库函数,new/delete是C++的运算符。
对于用户自定义的对象而言,用maloc/free无法满足动态管理对象的要求。对象在创建的同时要自动执行构造函数,对象在消亡之前要自动执行析构函数。
由于malloc/free是库函数而不是运算符,不在编译器控制权限之内,不能够把执行构造函数和析构函数的任务强加于malloc/free。
因此C++需要一个能完成动态内存分配和初始化工作的运算符new,以及一个能完成清理与释放内存工作的运算符delete。

volatile extern explicit 关键字

volatile

被设计用来修饰被不同线程访问和修改的变量。推荐一个定义为volatile的变量可能会被意想不到地改变,这样编译器就不会去假设这个变量值。
更准确地说,优化器在用到这个变量时必须每次都小心地重新读取这个值,而不是使用保存在寄存器里备份。
如果没有volatile关键字,则编译器可能优化读取和存储,可能暂时使用寄存器中的值,如果这个变量由别的程序更新了的话,将出现不一致的现象。

extern

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

explicit

explicit关键字只能用于修饰只有一个参数的类构造函数。它的作用是表明该构造函数是显示的, 而非隐式的, 跟它相对应的另一个关键字是implicit, 意思是隐藏的,类构造函数默认情况下即声明为implicit(隐式)。
例如:

class A  
{  
public:  
     explicit A(int a)  
    {  
        cout<<"创建类成功了!"<<endl;  
    }  

这样一来:

A a=10;

是不成功的,必须要显示地调用构造函数:

A a10);

extern “C”的作用

C++支持多态中的函数重载,会将函数名同参数一起生成中间的函数名。但是C不支持,所以同一函数被C++编译后在库中的名字与C语言是不同的。如果不添加extern “C”,函数可能找不到,它用来解决名字匹配的问题。
比如说你用C 开发了一个DLL 库,为了能够让C ++语言也能够调用你的DLL输出(Export)的函数,你需要用extern “C”来强制编译器不要修改你的函数名。

struct的地址对齐

常用变量所占字节

bool:1

char:1

short:2
int:4
long:4
long long:8

float:4
double:8

union的地址对齐

联合就是一个结构,它的所有成员相对于基地址的偏移量都为0,此结构空间要大到足够容纳最“宽”的成员,并且,其对齐方式要适合于联合中所有类型的成员。
例如:

union DATE
{
    char a;
    int i[5];
    double b;
};

sizeof(DATE)=24。
该结构要放得下int i[5]必须要至少占4×5=20个字节。如果没有double的话20个字节够用了,此时按4字节对齐。
但是加入了double就必须考虑double的对齐方式,double是按照8字节对齐的,所以必须添加4个字节使其满足8×3=24,也就是必须也是8的倍数,这样一来就出来了24这个数字。综上所述,最终联合体的最小的size也要是所包含的所有类型的基本长度的最小公倍数才行。

自然对界

struct 是一种复合数据类型,其构成元素既可以是基本数据类型(如int、long、float 等)的变量,也可以是一些复合数据类型(如array、struct、union 等)的数据单元。
对于结构体,编译器会自动进行成员变量的对齐,以提高运算效率。缺省情况下,编译器为结构体的每个成员按其自然对界(natural alignment)条件分配空间。各个成员按照它们被声明的顺序在内存中顺序存储,第一个成员的地址和整个结构的地址相同。
自然对界(natural alignment)即默认对齐方式,是指按结构体的成员中size 最大的成员对齐。
注意,找size最大的成员时不用考虑union和struct所占字节,但要考虑它们是按多少字节对齐的。具体见下面例子。

例如:

struct naturalalign
{
char a;
short b;
char c;
};

在上述结构体中,size 最大的是short,其长度为2 字节,因而结构体中的char 成员a、c 都以2 为单位对齐,sizeof(naturalalign)的结果等于6;
如果改为:

struct naturalalign
{
char a;
int b;
char c;
};

其结果显然为12。

typedef union {long i; int k[5]; char c;} DATE;
//20字节,按4个字节对齐。20字节能够装下k[5],同时能够让long和char对齐就可以。
struct data { int cat;  double dog;} too;
//16字节,其中double为8字节,所以是按8个字节对齐
struct data2 { char cat; int dag;} tt;
//8字节,其中int为4字节,所以按4个字节对齐,字符型按4字节对齐
struct data3 { char cat; double dag;} ;
//16字节
struct data4 { char cat; DATE dag;} ;
//24字节,其中联合体为20字节,按4个字节对齐,于是char也按4字节对齐
struct data5 { char cat; unsigned short int dag;};
//4字节,其中short为2字节,按2个字节对齐,char也按2字节对齐
struct data6 { int cat; unsigned short int dag;};
//8,其中整型为4字节,按四个字节对齐,短整型按4字节对齐
struct data7 { char cat; char *dag;};
//8,其中所有指针在内存均占4字节,按四个字节对齐,char也按4字节对齐
struct data8 { char cat; unsigned short int dag;int a;};
//8,为保证a按4字节对齐,前面必须也是4字节对齐,dag刚好为2字节,所以char后必须留出1字节的空间,这样就是8字节。
struct data9 { char cat; int a;unsigned short int x;};
//12,为保证a按4字节对齐,char后必须留出3字节的空间;同时为保证整个结构的自然对齐(这里是4字节对齐),在x后还要补齐2个字节,这样就是12字节。
struct data10 { int cat; unsigned short int dag;char a;};
//8个字节,char后面补1个字节
struct data10 { char a; double cat;int j;};
//24个字节,总体按4个字节对齐,char后面补7个字节,j后面补4个字节

空类所占地址空间

一个空类所占空间为1个字节,多重继承的空类所占空间也是1个字节,但是如果涉及到虚继承,那么类还会有一个虚指针,所以占4个字节。

内联函数和宏定义

inline是指嵌入代码,就是在调用函数的时候不跳转,而是把代码直接复制到那里去。它以牺牲代码量为代价,减少了普通函数调用时的资源消耗。
相比于宏定义,内联函数要做参数类型检查,而宏定义只是一个简单的替换。
inline关键字必须放在函数体的前面

const和宏定义

编译器处理方式

define宏在预处理阶段展开。
const变量在编译时确定其值。

存储方式

define宏仅仅是展开,有多少地方使用,就展开多少次,宏本身不会分配内存,宏展开后的立即数是会占用内存的。(展开多少次就分配多少次内存)
const变量在程序运行过程中只有一份拷贝(因为它是全局的只读变量,存放在静态区)。

类型和安全检查不同

define宏没有类型,不做任何类型检查,仅仅是展开。
const变量有具体的类型,在编译阶段会执行类型检查。

指针和引用的区别

是否可以为空

不能使用指向空值的引用,而指针可以为空。不存在指向空值的引用这一事实,意味着使用引用的代码效率比使用指针要高。

合法性

使用引用之前,不需要测试其合法性,而使用指针则应该先测试防止其为空。

可修改性

指针可以被重新赋值,以指向不同的对象,而引用则总是指向在初始化时被指定的对象,以后不能改变。

函数指针

就像数组名是指向数组第一个元素的指针一样,函数名也是指向函数的常指针。

long (* fun)(int)

这就是一个函数指针,这个指针的返回值是long,所带的参数是int。

int (*(*F)(int,int))(int)

F是一个指向函数的指针,它指向一种函数(该函数参数为int,int ,返回值为一个指针),返回的这个指针指向的是另一个函数,该函数的参数为int,返回值为int类型的函数。

double(*f[10])();

f是一个数组,有10个元素,每个元素都是函数指针。这些函数都没有形参,而且返回double。

指针数组和数组指针

int *ptr[]
//稍微改成int* ptr[] 更好理解

是指针数组,ptr[]里的元素是指针。

int (*ptr)[]

是指向整型数组的指针。

迷途指针(野指针、悬浮指针)

对一个指针进行delete操作后,释放了它所指向的内存,但没有把它设置为空时,产生迷途指针。
而后,如果你没有重新赋值就试图再次使用该指针,引起的后果是不可预料的。

空指针和迷途指针的区别是什么?

答:
当delete一个指针时,实际上仅是让编译器释放内存,但指针本身依然存在。这时它就是一个迷途指针。可以使用

ptr = 0;

来把迷途指针改为空指针。
使用迷途指针和空指针是非法的,而且有可能造成程序崩溃。相比而言,空指针的崩溃是一种可预料的崩溃。

句柄和指针

Windows系统用句柄标记系统资源,隐藏系统的信息,是一个32位的无符号整数,是指向指针的指针。
句柄是用来专门登记各应用对象在内存中的地址变化,而这个地址本身是不变的。

this指针

this指针本质上是一个函数参数,但是被编译器隐藏起来了。它只能在成员函数中使用,全局函数,静态函数都不能使用。

C++引入的额外开销

编译时开销

模板、类层次结构、强类型检查等新特性,以及大量使用了这些新特性的C++模板、算法库都明显增加了C++编译器的负担。

运行时开销

虚函数
RTTI
异常
对象的构造和析构

RTTI

RTTI是Runtime Type Information的缩写,字面理解就是执行时期的类型信息。其重要作用就是动态判别执行时期的类型。
通过RTTI,能够通过基类的指针或引用来检索其所指对象的实际类型。c++通过下面两个操作符提供RTTI。
(1)typeid:返回指针或引用所指对象的实际类型。
(2)dynamic_cast:将基类类型的指针或引用安全地转换为派生类型的指针或引用。

对于带虚函数的类,在运行时执行RTTI操作符,返回动态类型信息;对于其他类型,在编译时执行RTTI,返回静态类型信息。

当具有基类的指针或引用,但需要执行派生类操作时,需要动态的强制类型转换(dynamic_cast)。这种机制的使用容易出错,最好以虚函数机制代替之。

dynamic_cast 操作符

如果dynamic_cast转换指针类型失败,则返回0;如果转换引用类型失败,则抛出一个bad_cast类型的异常。
可以对值为0的指针使用dynamic_cast,结果为0。
dynamic_cast会首先验证转换是否有效,只有转换有效,操作符才进行实际的转换。

if (Derived *derivedPtr = dynamic_cast<Derived *>(basePtr))
{
    // use the Derived object to which derivedPtr points
}
else
{ // basePtr points at a Base object
    // use the Base object to which basePtr points
}

也可以使用dynamic_cast将基类引用转换为派生类引用:

dynamic_cast<Type&>(val)

因为不存在空引用,所以不能像指针一样对转换结果进行判断。不过转换引用类型失败时,会抛出std::bad_cast异常。

try
{ 
    const Derived &d = dynamic_cast<const Derived&>(b);
}
catch (bad_cast) {
    // handle the fact that the cast failed.
}

typeid操作符

typeid能够获取一个表达式的类型:typeid(e)。
如果操作数不是类类型或者是没有虚函数的类,则获取其静态类型;如果操作数是定义了虚函数的类类型,则计算运行时类型。
typeid最常见的用途是比较两个表达式的类型,或者将表达式的类型与特定类型相比较。

Base *bp;
Derived *dp;
// compare type at run time of two objects
if (typeid(*bp) == typeid(*dp))
{
    // bp and dp point to objects of the same type
}
// test whether run time type is a specific type
if (typeid(*bp) == typeid(Derived))
{
    // bp actually points a Derived
}

注意:如果是typeid(bp),则是对指针进行测试,这会返回指针(bp)的静态编译时类型(Base *)。
如果指针p的值是0,,并且指针所指的类型是带虚函数的类型,则typeid(*p)抛出一个bad_typeid异常。

STL的实现原理

vector

Vector是顺序容器,是一个动态数组,支持随机存取、插入、删除、查找等操作,在内存中是一块连续的空间。在原有空间不够情况下自动分配空间,增加为原来的两倍。vector随机存取效率高,但是在vector插入元素,需要移动的数目多,效率低下。
注意:vector动态增加大小时,并不是在原空间之后持续新空间(因为无法保证原空间之后尚有可供配置的空间),而是以原大小的两倍另外配置一块较大的空间,然后将原内容拷贝过来,然后才开始在原内容之后构造新元素,并释放原空间。因此,对vector的任何操作,一旦引起空间重新配置,指向原vector的所有迭代器就都失效了。

vector 中 size()和 capacity()的区别

size的意思是大小,此方法是返回该vector对象当前有多少个元素。
capacity的意思是容量,此方法返回的是该vector对象最多能容纳多少个元素。

map

Map是关联容器,以键值对的形式进行存储,方便进行查找。关键词起到索引的作用,值则表示与索引相关联的数据。以红黑树的结构实现,插入删除等操作都在O(logn)时间内完成。
注意:map的下标操作,其行为与vector很不相同:使用一个不在容器中关键字作为下标,会添加一个具有此关键字的元素到map中。一般使用find函数代替下标操作。

set

Set是关联容器,set中每个元素只包含一个关键字。set支持高效的关键字查询操作——检查一个给定的关键字是否在set中。set也是以红黑树的结构实现,支持高效插入、删除等操作。

隐藏和覆盖的区别

都是发生在基类和派生类之中的。但是它们之间最为重要的区别就是:
覆盖的函数是多态的,是存在于vtbl之中的函数才能构成”覆盖”的关系;覆盖是为了用基类的对象调用派生类的覆盖函数(名称、参数一致并指定了virtual关键字)。

而隐藏的函数都是一般的函数名相同的函数,其形参不需要相同。不支持多态,在编译阶段就已经确定下来了。
(1)如果派生类的函数与基类的函数同名,但是参数不同。此时,不论有无 virtual 关键字,基类的函数将被隐藏(注意别与重载混淆) 。
(2)如果派生类的函数与基类的函数同名,并且参数也相同,但是基类函数没有 virtual关键字。此时,基类的函数被隐藏(注意别与覆盖混淆) 。
可以认为,派生类的函数与基类的函数同名,而且不是虚函数,则基类的函数被隐藏。

友元

友元(frend)机制允许一个类将对其非公有成员的访问权授予指定的函数或者类,友元的声明以friend开始,它只能出现在类定义的内部,友元声明可以出现在类中的任何地方:友元不是授予友元关系的那个类的成员,所以它们不受其声明出现部分的访问控制影响。通常,将友元声明成组地放在类定义的开始或结尾是个好主意。

友元函数

友元函数是指某些虽然不是类成员函数却能够访问类的所有成员的函数。类授予它的友元特别的访问权,这样该友元函数就能访问到类中的所有成员。

class A  
{  
public:  
    friend void set_show(int x, A &a);      //该函数是友元函数的声明  
private:  
    int data;  
};  

void set_show(int x, A &a)  //友元函数定义,为了访问类A中的成员  
{  
    a.data = x;  
    cout << a.data << endl;  
}  
int main(void)  
{  
    class A a;  

    set_show(1, a);  

    return 0;  
}  

友元类

友元类的所有成员函数都是另一个类的友元函数,都可以访问另一个类中的隐藏信息(包括私有成员和保护成员)。当希望一个类可以存取另一个类的私有成员时,可以将该类声明为另一类的友元类。
关于友元类的注意事项:
(1) 友元关系不能被继承。
(2) 友元关系是单向的,不具有交换性。若类B是类A的友元,类A不一定是类B的友元,要看在类中是否有相应的声明。
(3) 友元关系不具有传递性。若类B是类A的友元,类C是B的友元,类C不一定是类A的友元,同样要看类中是否有相应的申明。

class A  
{  
public:  
    friend class C;                         //这是友元类的声明  
private:  
    int data;  
};  

class C             //友元类定义,为了访问类A中的成员  
{  
public:  
    void set_show(int x, A &a) { a.data = x; cout<<a.data<<endl;}  
};  

int main(void)  
{  
    class A a;  
    class C c;  

    c.set_show(1, a);  

    return 0;  
}  

友元成员函数

使类B中的成员函数成为类A的友元函数,这样类B的该成员函数就可以访问类A的所有成员了。
当用到友元成员函数时,需注意友元声明和友元定义之间的相互依赖,在该例子中,类B必须先定义,否则类A就不能将一个B的函数指定为友元。然而,只有在定义了类A之后,才能定义类B的该成员函数。更一般的讲,必须先定义包含成员函数的类,才能将成员函数设为友元。另一方面,不必预先声明类和非成员函数来将它们设为友元。

class A;    //当用到友元成员函数时,需注意友元声明与友元定义之间的互相依赖。这是类A的声明  
class B  
{  
public:  
    void set_show(int x, A &a);             //该函数是类A的友元函数  
};  

class A  
{  
public:  
    friend void B::set_show(int x, A &a);   //该函数是友元成员函数的声明  
private:  
    int data;  
    void show() { cout << data << endl; }  
};  

void B::set_show(int x, A &a)       //只有在定义类A后才能定义该函数,毕竟,它被设为友元是为了访问类A的成员  
{  
    a.data = x;  
    cout << a.data << endl;  
}  

int main(void)  
{  
    class A a;  
    class B b;  

    b.set_show(1, a);  

    return 0;  
}  

构造函数和析构函数的执行顺序

先执行基类的构造函数,随后执行派生类的构造函数;
当撤销派生类对象时,先执行派生类的析构函数,再执行基类的析构函数。

  • 1
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值