从C到C++,你必须要掌握的知识点(2017.06.05更新)

C++学习问题

内容原创,未经本人同意请勿转载。联系本人:jianshu_kevin@126.com

1,virtual函数

函数之前加上virtual关键字就表示该函数为虚函数,在派生类中通过重写该虚函数来实现对基类函数的覆盖。

    //基类中定义virtual函数
    class base
    {
    public:
        virtual void fun() {cout<<"BASE";}
    };
    //派生类中覆盖fun函数
    class derived: base
    {
    public:
        //不管该函数之前是否添加virtual字段,该函数都是虚函数
        void fun() {cout<<"DERIVED";}
    };
    /*多态使用*/
    int main()
    {
        base* d = new derived();
        /*类的多态,调用的是派生类中的fun函数*/
        d->fun();
    }
    //输出 
    DERIVED
    //
    void call_fun(base* b)
    {
        //如果b是base类的实例,就调用base中的fun
        //如果b是derived类的实例,就调用derived中的fun
        b->fun();
    }

为何"虚"---动态联编

virtual函数用到了动态联编推迟联编的技术,virtual函数在编译的时候是无法确定的,而是在运行的时候被确定的。
编译器在发现类中有virtual函数的时候,就会为该类分配一个VTABLE函数指针数组,这个数组里存放了类中的所有虚函数。

一个类只有一个VTABLE,不管有多少个实例
派生类有各自的VTABLE
同一个虚函数在基类和派生类的VTABLE的相同位置
编译器在编译的时候为每个实例在内存中分配一个vptr字段,该字段指向本实例的VTABLE

    void call_fun(base* b)
    {
        //如果b是base类的实例,就调用base中的fun
        //如果b是derived类的实例,就调用derived中的fun
        //编译后该函数b->fun();变成
        (base->vptr[1])();
        //这样根据传递进来的实例不同,就调用不同的函数。
    }

纯虚函数(interface类)

    class base
    {
    public:
        virtual fun() = 0; //0标志一个虚函数为纯虚函数  
    }

纯虚函数表示该类是一个抽象类,无法被实例化,只能被派生类继承覆盖。用来规范派生类,这就是所谓的“接口”类,用来约束派生类需要实现这些函数。

为什么有的析构函数必须写成虚函数

派生类的构造和析构函数的正常执行流程

  • 创建派生类,先执行基类的构造函数,再执行派生类的构造函数
  • 销毁派生类,先执行派生类的构造函数,再执行基类的构造函数
#include <iostream>

using namespace std;

class base
{
public:
    base(){cout<<"base structure"<<endl;}
    ~base(){cout<<"base destructure"<<endl;}
};

class sub : public base
{
public:
    sub(){cout<<"subclass structure"<<endl;}
    ~sub(){cout<<"subclass destructure"<<endl;}
};


int main()
{
    sub* ps = new sub();
    
    cout<<" "<<endl;
    
    delete ps;
    
    return 0;
}

///////////////执行结果//////////////////
base structure                      //创建
subclass structure

subclass destructure                //释放
base destructure
///////////////执行结果//////////////////

派生类的构造和析构函数的异常执行流程

main函数中的ps换成基类类型(多态特性),看看会有什么结果

int main()
{
    base* ps = new sub();
    
    count<<" "<<endl;
    
    delete ps;
    
    return 0;
}
///////////////执行结果//////////////////
base structure                    //创建
subclass structure

base destructure                  //释放,只调用了基类的析构函数
///////////////执行结果//////////////////

上面的代码改过后,发现释放的时候,只调用了基类的析构函数。但是构造的时候基类、派生类构造函数都被调用了。这就引发了内存泄露的问题

总结: 如果要用基类类型操作派生类,必须将析构函数写成virtual函数,否则会造成析构一半,从而出现内存泄漏。

基类操作的正常析构虚函数

#include <iostream>

using namespace std;

class base
{
public:
    base(){cout<<"base structure"<<endl;}
    virtual ~base(){cout<<"base destructure"<<endl;}
};

class sub : public base
{
public:
    sub(){cout<<"subclass structure"<<endl;}
    ~sub(){cout<<"subclass destructure"<<endl;}
};


int main()
{
    base* ps = new sub();
    
    count<<" "<<endl;
    
    delete ps;
    
    return 0;
}


///////////////执行结果//////////////////
base structure                      //创建
subclass structure

subclass destructure                //释放
base destructure
///////////////执行结果//////////////////

上面的代码只是在基类的析构函数~base()前加了virtual ~base(),在释放的时候,就能正常析构了。想想virtual函数的精髓,不难理解由于C++的多态特性,ps虽然是base类型,但是它由sub构造而成,所以析构的时候还是调用sub提供的析构函数。

2,命令空间

定义

    namespace ns1
    {
        int a;
        int b;
        class base{};
        void fun(){}
    }

分离式定义

one.h

    #ifndef TWO_H_
    #define TWO_H_
    namespace two
    {
        void say();
    }
    #endif

two.h

    #ifndef TWO_H_
    #define TWO_H_
    namespace two
    {
        void say();
    };
    #endif

one_two.cpp

    #include <iostream>
    #include "one.h"
    #include "two.h"
    void one::say()
    {
        cout<<"one say\r\n";
    }
    void two::say()
    {
        cout<<"two say\r\n";
    }
    //如果声明的空间有类如何实现????

使用

若想使用某个标识符,using 空间名::标识符;
若想使用改namespace下的所有标识符 using namespace 空间名;

    //方法1
    using namespace one;
    //方法2
    using one::say;

自定义空间名使用

    #include <iostream>
    #include <string>
    using namespace std;
    using namespace one;
    using namespace two;

    //全局函数
    void say()
    {
        cout<<"global say\r\n";
    }
    int main()
    {
        //全局函数,一定要加上::
        //否则会出现 错误:调用重载的‘say()’有歧义
        ::say();
        one::say();
        two::say();
    }

3,模板

模板可以实现逻辑相同,但是数据类型不同的代码复制。当用户使用模板时,参数由编译器决定,这很像
模板分为函数模板类模板

函数模板

定义&使用

    template <类型参数表> 返回类型 函数名(形参列表) {函数实体}
    
    template <typename T> void print(const T& var)
    {
        cout<<var<<endl;
    }

    int main()
    {
        String a("hello template");
        int num = 123;
        print(a);
        print(num);
    }
    /**********多个参数***********/
    template <class T> T min(T ii, T jj, T kk)
    {
        T temp;
        if(ii < jj) temp = ii;
        else temp = jj;
        if(temp > kk) temp = kk;
        return temp;
    }
    int main()
    {
        int minNum = min(100, 30, 102);
        cout<<minNum<<endl;
        char minChar = min('z', 'a', 'h');
        cout<<minChar<<endl;
        return 0;
    }

类模板

定义和使用

templete <类型参数列表> class base{};

4,操作符重载

4.1,怎么定义该函数

使用operator xx(xx表示操作符,例如==,+,-,等

    class person{
    private:
        int age;
    public:
        person(int a):age(a){};
        inline operator == (const person& p) const;
    };

    inline person::operator==(const person& p) const
    {
        if(this->age == p.age)
        {
            return true;
        }
        else
        {
            return false;
        }
    }

    using namespace std;
    void main()
    {
        person p1(10);
        person p2(20);
        
        if(p1 == p2)
        {
            cout<<"the age equal"<<endl;
        }
        else
        {
            cout<<"the age different"<<endl;
        }
    }

4.2,为什么要重载

对于系统的所有操作符,一般情况下,只支持基本数据类型和标准库中提供的class,对于用户自己定义的class,如果想支持基本操作,比如比较大小,判断是否相等,等等,则需要用户自己来定义关于这个操作符的具体实现。比如,判断两个人是否一样大,我们默认的规则是按照其年龄来比较,所以,在设计person 这个class的时候,我们需要考虑操作符==,而且,根据刚才的分析,比较的依据应该是age。那么为什么叫重载呢?这是因为,在编译器实现的时候,已经为我们提供了这个操作符的基本数据类型实现版本,但是现在他的操作数变成了用户定义的数据类型class,所以,需要用户自己来提供该参数版本的实现。

4.3,操作符重载为全局函数

估计没人会这么使用

对于全局的操作符重载函数,左操作数的参数必须被显式的定义。

bool operator == (person const &p1, person const &p2);

如何决定使用全局还是类操作符重载函数呢?

  • 1 左操作符和比较对象是不是同一个类型
  • 2 C++要求,=、[]、()、->、必须定义为类操作符重载函数

5,重载函数

5.1 什么叫重载函数?

在同一个作用域内,可以有一组具有名字相同,参数不同的一组函数。重载函数用来命名一组功能相似的函数。

5.2 为什么要用重载函数

  • 必须写很多函数名,来完成功能相似的一组函数
  • 类构造函数,如果没有重载。那如果要实例化不同的类是比较麻烦的。
  • 操作符重载本身也是函数重载,丰富了已有操作符的功能。

5.3 编译器如何解决命名冲突

编译器会把不同参数名字相同的函数,用新的函数名取代。恩,其实也就还是不同名字,但是写起来方便很多

5.4 重写函数(override)

子类重新定义父类中有相同名称、相同参数虚函数

  • 被重新定义的函数不能为static函数
  • 重写的函数一定要完全相同(包括返回值、参数)
  • 重写的函数可以有不同的修饰符,例如在基类中是private,派生类可以写成public、protected

5.5 重定义函数(redefining)

子类重新定义父类中具有相同名字的函数(参数列表可以不同)。

6,static类

6.1 static成员函数

static数据成员是存储在程序的静态存储区,而并不是在栈空间上。独立于任何类的对象。

注意:static成员函数

  • 没有this指针。因为static成员函数不是任何对象的组成部分
  • 不能声明为const类型,不能访问非static成员,也不能访问static const成员。

6.2 static成员

注意:在类中不能对static成员进行初始化,除非写成const static

    class person{
    private:
        static int age;
        //static int age= 20; //错误:ISO C++ 不允许在类内初始化非常量静态成员'person::age'
        const static int c_int = 100; //允许
        static string name;
    public:
        void print()
        {
            cout<<"name: "<<name<<" age: "<<age<<endl;
        }
    }

    int person::age = 30;
    string person::name = "kevin";

    int main()
    {
        //int person::age = 20;  错误:对限定名‘person::age’的使用无效
        person p;
        p.print();
        return 0;
    }

6.3 static和const不能同时修饰一个成员函数

因为static是属于的,而const函数是为了保证改函数不会修改对象中的成员。两个关键字作用的对象不一致,所以不能放到一起使用。

内容原创,未经本人同意请勿转载。联系本人:jianshu_kevin@126.com

7,引用

引用就相当于给一个变量取了另外一个名字(alias),对应操作和直接操作原变量效果是一样的。

  • 声明一个引用时一定要进行初始化
  • 引用本身不占用内存,系统也不会给引用分配地址

7.1 引用和指针在函数参数的区别

相同点

使用指针和引用传递参数,对于运算效果来说一样的。所有对形参的操作都会直接反映到原变量当中。

不同点

指针作为形参时,函数需要给形参分配地址。而且操作中需要经常用到*指针变量的方式进行运算,代码可读性比较差。
形参作为形参时,函数在内存中并没有产生额外的地址,而是对实参直接操作。

//常量引用
int a = 100;
const int &ra = a;

//错误,常量引用不允许赋值操作
ra = 1;
//正确
a = 1;

7.2 引用作为函数返回值

网上的一些说法

    1. 不能返回局部变量的引用(函数执行完,内存就会被释放)
    1. 不能返回new出来的数据(虽然内存还在,但是会造成无法通过delete释放引用的问题)

对这个说法有点怀疑,自己实际测试结果

  • 测试一,局部变量的引用返回给另外一个引用
int* & ref_pointer(const int* b)
{
    int* pI = (int*)malloc(100);
    cout<<"pointer addr = "<<b<<endl;
    cout<<"malloc p = "<<pI<<endl;
    return pI;
}

int main()
{
    int a = 100;
    
    int* &refP = ref_pointer(&a);
    cout<<"return ref_pointer addr = "<<refP<<endl;
    
    
    return 0;
}

////////////////编译会报警告,提示不能返回局部变量的引用////////////////
//g++ 警告:返回了对局部变量的‘pI’的引用 [-Wreturn-local-addr]

///////////////执行结果//////////////////
pointer addr = 0x61ac28
malloc p = 0x20010308
return ref_pointer addr = 0x20010308
///////////////执行结果//////////////////
//

执行结果没有出现问题,感觉局部变量的引用没有问题,这是因为虽然refP是引用了局部变量pI,pI是放在堆栈中的,但是我们的代码返回后,这部分空间并没有被再次利用,所以就会出现看起来运行并没有问题。但是当我们打开test函数的时候,发现问题了:

int main()
{
    int a = 100;
    
    int* &refP = ref_pointer(&a);
    test();
    cout<<"return ref_pointer addr = "<<refP<<endl;
    
    
    return 0;
}

///////////////执行结果//////////////////
pointer addr = 0x61ac28
malloc p = 0x20010308
return ref_pointer addr = 0
///////////////执行结果//////////////////

test函数执行后会从新覆盖堆栈,这就导致了局部变量pI被覆盖了,所以refP引用也就失效了。

  • 测试二,局部变量的引用返回给变量
int main()
{
    int a = 100;
    
    int* refP = ref_pointer(&a);
    test();
    cout<<"return ref_pointer addr = "<<refP<<endl;
    
    
    return 0;
}

///////////////执行结果//////////////////
pointer addr = 0x61ac28
malloc p = 0x20010308
return ref_pointer addr = 0x20010308
///////////////执行结果//////////////////

从返回结果看,虽然再次调用了test但是依然没有问题,并不像网上说的,不能返回局部变量的引用

总结:1. 引用本质上就是一个常量指针,引用本身是占用地址空间的,对引用的运算,被直接转化成对原变量地址内容的操作。
2. 局部变量在return的时候已经完成了给左值赋值,除非返回给了另外一个引用,否则也不会出现问题。但是不同版本的编译器可能不一样
我编译的时候,已经出现warning了,所以为了代码更强的通用性,最好也不要这么用。

type & fun();
//正确写法
type a = fun();
//错误写法
type &a = fun();

8,this

类中的成员函数,都有一个附件的隐含实参,该实参(this)就是一个指向该类对象的指针。

9,#include xx.h文件和xx有何区别

10,访问权限

三种访问权限

  • public 可以被任意实体访问
  • protected 只允许子类和本类成员函数访问
  • private 只允许本类成员函数访问

三种继承方式

  • public继承 不改变基类成员的访问权限
  • protected继承 使得基类中public变成protected,其他权限不变。
  • private继承 使得基类中所有成员权限变成private
    class base{};
    class deliverd : public base{}; //public继承

11, 重载和覆盖

12, new delete

12.1 和malloc、free有何区别

和malloc,free一样是用来动态分配内存的,不同的是new和delete会自动调用对象的构造和析构函数。

12.2 new[], delete[]

没错这个是针对数组的操作,delete操作只会调用一次析构函数,而delete[]会调用每个成员的析构函数。一般new[]和delete[]配对使用。

13, const成员函数

若将成员成员函数声明为const,则该函数不允许修改类的数据成员

1)const成员函数可以访问非const对象的非const数据成员、const数据成员,也可以访问const对象内的所有数据成员;
2)非const成员函数可以访问非const对象的非const数据成员、const数据成员,但不可以访问const对象的任意数据成员;
3)作为一种良好的编程风格,在声明一个成员函数时,若该成员函数并不对数据成员进行修改操作,应尽可能将该成员函数声明为const 成员函数。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,772评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,458评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,610评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,640评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,657评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,590评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,962评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,631评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,870评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,611评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,704评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,386评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,969评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,944评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,179评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,742评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,440评论 2 342

推荐阅读更多精彩内容

  • 前言 把《C++ Primer》[https://book.douban.com/subject/25708312...
    尤汐Yogy阅读 9,504评论 1 51
  • C++文件 例:从文件income. in中读入收入直到文件结束,并将收入和税金输出到文件tax. out。 检查...
    SeanC52111阅读 2,751评论 0 3
  • 1. 让自己习惯C++ 条款01:视C++为一个语言联邦 为了更好的理解C++,我们将C++分解为四个主要次语言:...
    Mr希灵阅读 2,782评论 0 13
  • 1. 结构体和共同体的区别。 定义: 结构体struct:把不同类型的数据组合成一个整体,自定义类型。共同体uni...
    breakfy阅读 2,110评论 0 22
  • 1. C++基础知识点 1.1 有符号类型和无符号类型 当我们赋给无符号类型一个超出它表示范围的值时,结果是初始值...
    Mr希灵阅读 17,926评论 3 82