首页 > 代码库 > CPP 学习笔记随笔

CPP 学习笔记随笔

CPP对象模型

何为C++对象模型?

C++对象模型可以概括为以下2部分:

  1. 语言中直接支持面向对象程序设计的部分

  2. 对于各种支持的底层实现机制

语言中直接支持面向对象程序设计的部分,如构造函数、析构函数、虚函数、继承(单继承、多继承、虚继承)、多态等等,这也是组里其他同学之前分享过的内容。第一部分这里我简单过一下,重点在底层实现机制。

在c语言中,“数据”和“处理数据的操作(函数)”是分开来声明的,也就是说,语言本身并没有支持“数据和函数”之间的关联性。在c++中,通过抽象数据类型(abstract data type,ADT),在类中定义数据和函数,来实现数据和函数直接的绑定。

概括来说,在C++类中有两种成员数据:static、nonstatic;三种成员函数:static、nonstatic、virtual。

技术分享

如下面的base类定义:

#pragma once
#include<iostream>
using namespace std;
class Base
{
public:
    Base(int);
    virtual ~Base(void);

    int getIBase() const;
    static int instanceCount();
    virtual void print() const;

protected:

    int iBase;
    static int count;
};

基本C++对象模型

在介绍C++使用的对象模型之前,介绍2种对象模型:简单对象模型(a simple object model)、表格驱动对象模型(a table-driven object model)。

简单对象模型(a simple object model)

技术分享

所有的成员占用相同的空间(跟成员类型无关),对象只是维护了一个包含成员指针的一个表。表中放的是成员的地址,无论上成员变量还是函数,都是这样处理。对象并没有直接保存成员而是保存了成员的指针。

表格对象模型(a table-driven object model)

技术分享

这个模型在简单对象的基础上又添加了一个间接层。将成员分成函数和数据,并且用两个表格保存,然后对象只保存了两个指向表格的指针。这个模型可以保证所有的对象具有相同的大小,比如简单对象模型还与成员的个数相关。其中数据成员表中包含实际数据;函数成员表中包含的实际函数的地址(与数据成员相比,多一次寻址)。

C++对象模型

技术分享

这个模型从结合上面2中模型的特点,并对内存存取和空间进行了优化。在此模型中,non static 数据成员被放置到对象内部,static数据成员, static and nonstatic 函数成员均被放到对象之外。对于虚函数的支持则分两步完成:

  1. 每一个class产生一堆指向虚函数的指针,放在表格之中。这个表格称之为虚函数表(virtual table,vtbl)。

  2. 每一个对象被添加了一个指针,指向相关的虚函数表vtbl。通常这个指针被称为vptr。vptr的设定(setting)和重置(resetting)都由每一个class的构造函数,析构函数和拷贝赋值运算符自动完成。

另外,虚函数表地址的前面设置了一个指向type_info的指针,RTTI(Run Time Type Identification)运行时类型识别是有编译器在编译器生成的特殊类型信息,包括对象继承关系,对象本身的描述,RTTI是为多态而生成的信息,所以只有具有虚函数的对象在会生成。

这个模型的优点在于它的空间和存取时间的效率;缺点如下:如果应用程序本身未改变,但当所使用的类的non static数据成员添加删除或修改时,需要重新编译。

漫谈CPP—良好的编程习惯与编程要点

以良好的方式编写C++ class

假设现在我们要实现一个复数类complex,在类的实现过程中探索良好的编程习惯。

① Header(头文件)中的防卫式声明
complex.h:

//防止头文件的内容被多次包含
# ifndef  __COMPLEX__
# define __COMPLEX__
class complex
{

};
# endif

② 把数据放在private声明下,提供接口访问数据

# ifndef  __COMPLEX__
# define __COMPLEX__
class complex
{
    public:
        double real() const {return re;}
        double imag() const {return im;}
    private:
        doubel re,im;
}
# endif

③ 不会改变类属性(数据成员)的成员函数,全部加上const声明

例如上面的成员函数:

double real () `const` {return re;}
double imag() `const` {return im;}

既然函数不会改变对象,那么就如实说明,编译器能帮你确保函数的const属性,阅读代码的人也明确你的意图。

而且,const对象才可以调用这些函数——const对象不能够调用非const成员函数。

④ 使用构造函数初始值列表

class complex
{
    public:
        complex(double r = 0, double i =0)
            : re(r), im(i)  { }
    private:
        doubel re,im;
}

在初始值列表中,才是初始化。在构造函数体内的,叫做赋值。

⑤如果可以,参数尽量使用reference to const

为complex 类添加一个+=操作符:

class complex
{
    public:
        complex& operator += (const complex &)
}

使用引用避免类对象构造与析构的开销,使用const确保参数不会被改变。内置类型的值传递与引用传递效率没有多大差别,甚至值传递效率会更高。例如,传递char类型时,值传递只需传递一个字节;引用实际上是指针实现,需要四个字节(32位机)的传递开销。但是为了一致,不妨统一使用引用。

⑥ 如果可以,函数返回值也尽量使用引用

以引用方式返回函数局部变量会引发程序未定义行为,离开函数作用域局部变量被销毁,引用该变量没有意义。但是我要说的是,如果可以,函数应该返回引用。当然,要放回的变量要有一定限制:该变量的在进入函数前,已经被分配了内存。以此条件来考量,很容易决定是否要放回引用。而在函数被调用时才创建出来的对象,一定不能返回引用。

说回operator +=,其返回值就是引用,原因在于,执行a+=b时,a已经在内存上存在了。

而operator + ,其返回值不能是引用,因为a+b的值,在调用operator +的时候才产生。

下面是operator+= 与’operator +’ 的实现:

inline complex & complex :: operator += (const complex & r)
{
        this -> re+= r->re;
        this -> im+= r->im;
        return * this;
}
inline complex operator + (const complex & x , const complex & y)
{
        return complex ( real (x)+ real (y),                        //新创建的对象,不能返回引用
                                 imag(x)+ imag(y));
}

⑧ 提供给外界使用的接口,放在类声明的最前面

这是某次面试中,面试官大哥告诉我的。想想确实是有道理,类的用户用起来也舒服,一眼就能看见接口。

Class with pointer member(s):记得写Big Three

C++的类可以分为带指针数据成员与不带指针数据成员两类,complex就属于不带指针成员的类。而这里要说的字符串类String,一般的实现会带有一个char *指针。带指针数据成员的类,需要自己实现class三大件:拷贝构造函数、拷贝赋值函数、析构函数。

class String
{
    public:
        String (const char * cstr = 0);
        String (const String & str);
        String & operator = (const String & str);
        ~String();
        char * get_c_str() const {return m_data};
    private:
        char * m_data;
}

如果没有写拷贝构造函数、赋值构造函数、析构函数,编译器默认会给我们写一套。然而带指针的类不能依赖编译器的默认实现——这涉及到资源的释放、深拷贝与浅拷贝的问题。在实现String类的过程中我们来阐述这些问题。

①析构函数释放动态分配的内存资源

如果class里有指针,多半是需要进行内存动态分配(例如String),析构函数必须负责在对象生命结束时释放掉动态申请来的内存,否则就造成了内存泄露。局部对象在离开函数作用域时,对象析构函数被自动调用,而使用new动态分配的对象,也需要显式的使用delete来删除对象。而delete实际上会调用对象的析构函数,我们必须在析构函数中完成释放指针m_data所申请的内存。下面是一个构造函数,体现了m_data的动态内存申请:

/*String的构造函数*/
inline 
String ::String (const char *cstr = 0)
{
    if(cstr)
    {
        m_data = http://www.mamicode.com/new char[strlen(cstr)+1];   // 这里,m_data申请了内存
        strcpy(m_data,cstr);
    }
    else
    {
        m_data= http://www.mamicode.com/new char[1];
        *m_data = http://www.mamicode.com/‘\0‘;
    }
}

这个构造函数以C风格字符串为参数,当执行

String *p = new String ("hello");
m_data向系统申请了一块内存存放字符串hello:

技术分享

析构函数必须负责把这段动态申请来的内存释放掉:

inline 
String ::~String()
{
    delete[]m_data;
}

②赋值构造函数与复制构造函数负责进行深拷贝

来看看如果使用编译器为String默认生成的拷贝构造函数与赋值操作符会发生什么事情。默认的复制构造函数或赋值操作符所做的事情是对类的内存进行按位的拷贝,也称为浅拷贝,它们只是把对象内存上的每一个bit复制到另一个对象上去,在String中就只是复制了指针,而不复制指针所指内容。现在有两个String对象:

String a("Hello");
String b("World");

a、b在内存上如图所示:

技术分享

如果此时执行
b = a;

浅拷贝体现为:

技术分享

存储World\0的内存块没有指针所指向,已经成了一块无法利用内存,从而发生了内存泄露。不止如此,如果此时对象a被删除,使用我们上面所写的析构函数,存储Hello\0的内存块就被释放调用,此时b.m_data成了一个野指针。来看看我们自己实现的构造函数是如何解决这个问题的,它复制的是指针所指的内存内容,这称为深拷贝。

/*拷贝赋值函数*/
inline String &String ::operator= (const String & str)
{
    if(this == &str)           //①
        return *this;
    delete[] m_data;        //②
    m_data = http://www.mamicode.com/new char[strlen(str.m_data)+1];        //③
    strcpy(m_data,str.m_data);            //④
    return *this
}

这是拷贝赋值函数的经典实现,要点在于:

① 处理自我赋值,如果不存在自我赋值问题,继续下列步骤:
② 释放自身已经申请的内存
③ 申请一块大小与目标字符串一样大的内存
④ 进行字符串的拷贝

对于a = b,②③④过程如下:

技术分享

技术分享

技术分享

同样的,复制构造函数也是一个深拷贝的过程:

inline String ::String(const String & str )
{
    m_data = http://www.mamicode.com/new char[ strlen (str) +1];
    strcpy(m_data,str.m_data);
}

另外,一定要在operator = 中检查是否self assignment 假设这时候确实执行了对象的自我赋值,左右pointers指向同一个内存块,前面的步骤②delete掉该内存块造成下面的结果。当企图对rhs的内存进行访问是,结果是未定义的。

技术分享

同样的,复制构造函数也是一个深拷贝的过程:

inline String ::String(const String & str )
{
    m_data = http://www.mamicode.com/new char[ strlen (str) +1];
    strcpy(m_data,str.m_data);
}

另外,一定要在operator = 中检查是否self assignment 假设这时候确实执行了对象的自我赋值,左右pointers指向同一个内存块,前面的步骤②delete掉该内存块造成下面的结果。当企图对rhs的内存进行访问是,结果是未定义的。

技术分享

static与类

① 不和对象直接相关的数据,声明为static

想象有一个银行账户的类,每个人都可以开银行账户。存在银行利率这个成员变量,它不应该属于对象,而应该属于银行这个类,由所有的用户来共享。static修饰成员变量时,该成员变量放在程序的全局区中,整个程序运行过程中只有该成员变量的一份副本。而普通的成员变量存在每个对象的内存中,若把银行利率放在每个对象中,是浪费了内存。

② static成员函数没有this指针

static成员函数与普通函数一样,都是只有一份函数的副本,存储在进程的代码段上。不一样的是,static成员函数没有this指针,所以它不能够调用普通的成员变量,只能调用static成员变量。普通成员函数的调用需要通过对象来调用,编译器会把对象取地址,作为this指针的实参传递给成员函数:

obj.func() —> Class :: fun(&obj);
而static成员函数即可以通过对象来调用,也可以通过类名称来调用。

③在类的外部定义static成员变量

另一个问题是static成员变量的定义。static成员变量必须在类外部进行定义:

class A
{
    private:
        static int a; //①
}
int A::a = 10;  //②

注意①是声明,②才是定义,定义为变量分配了内存。

④static与类的一些小应用

这些可以用来应付一下面试,在实现单例模式的时候,static成员函数与static成员变量得到了使用,下面是一种称为”饿汉式“的单例模式的实现:

class A
{
        public:
            static A& getInstance();
            setup(){...};
        private:
            A();
            A(const A & rhs);
            static A a;
}

这里把class A的构造函数都设置为私有,不允许用户代码创建对象。要获取对象实例需要通过接口getInstance。”饿汉式“缺点在于无论有没有代码需要a,a都被创建出来。下面是改进的单例模式,称为”懒汉式“:

class A
{
    public: 
        static  A& getInstance();
        setup(){....};
    private:
        A();
        A(const A& rsh);
        ...
};
A& A::getInstance()
{
        static A a;
        return a;
}

“懒汉式”只有在真正需要a时,调用getInstance才创建出唯一实例。这可以看成一个具有拖延症的单例模式,不到最后关头不干活。很多设计都体现了这种拖延的思想,比如string的写时复制,真正需要的时候才分配内存给string对象管理的字符串。

最全面的 C++ 资源、框架大全

分享—— C++ 框架、库和资源的一些汇总列表,内容包括:标准库、Web应用框架、人工智能、数据库、图片处理、机器学习、日志、代码分析等——资料全集。

最值得关注的10个C开源项目——资源

C++中引用和匿名对象的理解和本质剖析

大家对C++的引用应该都不陌生吧,抱着既要知其然,也要知其所以然的态度。下面将按照是什么?怎么用?为什么需要?本质剖析的流程来向大家一一描述。

引用是什么?

引用其实就是给变量起的一个别名,使用这个别名跟使用变量名没有区别。

那什么又是变量名呢?

变量名实质上是一段连续存储空间的别名,是一个标号(门牌号),编译器通过变量来申请并命名内存空间,程序员可以通过变量的名字可以使用存储空间。

也可以这样理解,变量名是逻辑概念,变量是物理层面,变量含数据类型和数据值,数据类型决定内存的分配,编译器将变量名和变量对应的内存联系起来,使程序员可以通过变量名来操作内存。

引用怎么用?

语法:Type& name = var;

规则:

  1. 普通引用在声明时必须用其它的变量进行初始化

  2. 引用作为函数参数声明时不进行初始化(后面将通过引用本质来解释原因)

为什么需要引用?

1)引用作为其它变量的别名而存在,因此在一些场合可以代替指针

2)引用相对于指针来说具有更好的可读性和实用性

引用为java等高级的语言程序员提供了很大便利,其不需要了解C++中的指针,只需要按照以前的习惯来使用就可以。

引用的本质剖析(很重要!!)

1、引用其实是个常量,证明如下

int main()
{
    int a = 1;
    //int& b;   C++编译器提示:错误“b”,必须初始化引用-->说明引用是个常量
    int& b = a;
}

说明: 必须初始化引用–>说明引用是个常量

2、引用其实也是个指针,证明如下

struct teacher
{
    int age;        //4个字节
    teacher& m_techer;  
};

struct student
{
    int age;               //4个字节
    short& weight;
};

int main()
{
    cout说明m_techer的
                                                        引用占4个字节*/
    cout说明weight的引
                                                        用占4个字节*/
    system("pause");
    return 0;
}

说明:从上面teacher&和short&的两个引用中占用的4个字节(32位系统),可以推断出引用其实是个指针。

根据1、2的结论可以推断出引用其实是个指针常量或者是常量指针,下面进一步证明。

3、引用其实是个指针常量 ,证明如下

int main()
{
    int a =10;
    int m = 22;

    int& b = a;
    &b = &m;      /*疑问:  b是引用,引用是个指针,指针赋值为什么还要在取地址符&b
                        (因为编译器在我们使用引用时,自动给引用披上了间接引用的外衣即:*b)
                    编译错误 “=”: 左操作数必须为左值-->引用是个指针常量,不能修改
                      其指针的指向。*/

    system("pause");
    return 0;
}

说明:引用是个指针常量。下面会说出C++编译器是怎么在C语言的基础上加入引用机制的。

4、C++编译器在C语言的基础上加入引用机制

技术分享

说明:

1、声明引用时,C语言将引用声明的是指针常量。(为啥要初始化引用原因)

2、引用使用,C语言隐藏了对常指针自动间接引用,让我们完全不用了解指针

3、初始化引用时,C语言隐藏了对变量的取地址符&操作,让我们感觉是在直接给变量起别名

应用的剖析到此就结束了,下面我们来说说匿名对象吧。

什么是匿名对象

匿名对象可以理解为是一个临时对象,一般系统自动生成的,如你的函数返回一个对象,这个对象在返回时会生成一个临时对象。

匿名对象的生命周期(很重要!!!)

class Cat
{
public:
    Cat()
    {
        cout<<"Cat类 无参构造函数"<<endl;
    }

    Cat(Cat& obj)
    {
        cout<<"Cat类 拷贝构造函数"<<endl;
    }

    ~Cat()
    {
        cout<<"Cat类 析构函数 "<<endl;
    }

};

void playStage() //一个舞台,展示对象的生命周期
{
    Cat();             /*在执行此代码时,利用无参构造函数生成了一个匿名Cat类对象;执行完此行代码,
                            因为外部没有接此匿名对象的变量,此匿名又被析构了*/
    Cat cc = Cat();    /*在执行此代码时,利用无参构造函数生成了一个匿名Cat类对象;然后将此匿名变
                            成了cc这个实例对象,此匿名对象没有被析构。*/
    cout<<"cc 对象好没有被析构"<<endl;    
}

int main()
{
    playStage();
    system("pause");
    return 0;
}

输出:

Cat类 无参构造函数
Cat类 析构函数
Cat类 无参构造函数
cc 对象好没有被析构
Cat类 析构函数

说明:

1、在执行playStage( )函数中的Cat( )时,生成了一个匿名对象,执行完Cat( )代码后,此匿名对象就此消失。这就是匿名对象的生命周期。

2、在执行playStage( )函数中Cat cc = Cat();时,首先生成了一个匿名对象,因为外部有cc对象在等待被实例化,然后将此匿名对象变为了cc对象,其生命周期就变成了cc对象的生命周期。

总结:

如果生成的匿名对象在外部有对象等待被其实例化,此匿名对象的生命周期就变成了外部对象的生命周期;如果生成的匿名对象在外面没有对象等待被其实例化,此匿名对象将会生成之后,立马被析构。

易被遗忘的C/C++要点总结

数据类型及运算

求补码

  • 原码的基础上, 符号位不变, 其余各位取反, 最后+1
  • 原码转补码不考虑符号位
  • 补码转原码,符号位不参与运算
  • 取反后 + 1 == 取反前 – 1

科学计数法表示

  • 1.8 * 10^11 –> 1.8E11
  • 9.34 * 10^-3 –> 9.34E-3

相关细节

  • sizeof()是一个运算,而非函数
  • ++运算不能用在实数上
  • 判断一个整数是否是2^n(2,4,6,8,16…)
    • !(x & (x – 1))
  • 三目条件运算符代码更优
    • 编译器能产生比if…else…更优的代码

运算符优先级、结合方向规则

  • 单目 > 双目
  • 算术 > 关系 > 位 > 逻辑 > 条件(三目)> 赋值 > 逗号
    • 算术: + – * /
    • 关系: > < >= <=
    • 位: & | ^
    • 单目: ~
    • 逻辑: && ||
    • 单目: !
  • 自右向左的三种运算符
    • 单目
    • 赋值
    • 条件

数据输入与输出

  • printf()语句从右向左计算输出表达式的值
 i = 1;
  printf("%d, %d\\\\n", i++, i--);
  //res: 0,1
  //先执行i--,再执行i++

常用输出函数

  • printf( )
  • putchar( )
    • 输出一个字符
    • 必须是字符型变量或常量
  • puts( )
    • 输出一个字符串
    • 必须是字符串或常量

常用输入函数

  • scanf( )
  • gets( )
    • 每次读取一个字符串
  • getche()
    • conio.h中
    • 读取字符不用按回车
  • getchar()
    • stdio.h中
    • 完成后须按回车
  • getche() & getchar():
    • 每次读取一个字符
  • scanf() & gets()区别:
    • scanf不能输入含空格字符串,gets可以

技术分享

选择语句和循环语句

switch:case 常量表达式

  • 常量表达式只能为整型、字符型
  • 不允许浮点型

数组

定义

  • 定义数组未赋初值
    • Turbo C会给数组置0
    • VC则取随机值
  • 定义静态数组,则系统自动赋0

比较字符串数组中的值

  • C:     strcmp(str1,str2)
  • C++:    str1 == str2
  • JAVA:   str1.equals(str2)
    • java中,str1 == str2 比较的是地址

四、指针

指针运算

  • 指针相减: 表示两指针所指地址之间的数据个数
  • 指针相加: 没有意义,错误

数组与指针

1、一维数组首地址

int a[10], *p;
p = &a[0];
p = a
//等价,将数组首元素的首地址赋给指针p
  • 表示:&a[0], a:  数组首元素的首地址
    &a:       数组首地址

  • 对比:

a == &a[0]
a != &a    //地址值相同,含义不同

2、二维数组首地址

int a[10][10];

地址值相同,含义不同:
a:

  • 二维数组首元素首地址
  • 代表一维数组元素的首地址

&a:

  • 数组首地址

&a[0]:

  • 二维数组首元素首地址

&a[0][0]:

  • &a[0][0] != a
  • a[0] == &a[0][0]

3、二维数组指针

  • int (*p)[3]:
    • 指向含3个元素的二维数组的行指针
    • 数组每列有3个元素
  • int \p[3] & int (p[3]):
    • 指针数组,每个元素均是一个指针

指针与引用的区别

  • 非空区别
    • 引用必须总是指向某些对象
      不能使用指向空值的引用
      不存在指向空值的引用
      效率比使用指针高
    • 指针可以指向空值
  • 合法性区别
    • 使用引用前,无需测试其合法性
    • 使用指针总是需要判空
  • 可修改区别
    • 指针可被重新赋值,以指向另一对象
    • 引用
      总指向初始化时被指定的对象
      以后都不能改变
      但指定对象的内容可以改变
  • 应用区别
    • 指针场景
      存在不指向任何对象的情况
      不同的时刻指向不同对象的情况
    • 引用场景
      指向一个对象后就不会改变指向的情况

ps:声明引用 / const常量 的同时,必须初始化

函数指针

  • float(**def)[10];
    • 二级指针
    • 指向一个一维数组的指针
    • 数组元素都是float
  • double(\gh)[10];
    • 指针gh,指向一个一维数组
    • 该数组元素的类型均为double *
  • double(*f[10])();
    • f是一个数组,含10个元素
      元素都是函数指针
    • 指向的函数
      没有参数
      返回double类型的值
  • int ( (\b)[10] );
    • 和int (\b)[10]一样
  • Long (* fun)(int)
    • 函数指针

五、类型转换

(int &)相关

  • float a = 1.0f;
  • (int)a实际上是以浮点数a为参数构造了一个整型数,该整数的值是1。
  • (int&)a则是告诉编译器将a当作整数看(并没有做任何实质上的转换)。

unsigned int

  • unsigned int a = 0xFFFFFFF7;
  • unsigned char i = (unsigned char)a;
    • i: 000000f7
  • char \b = (char )&a;
    • *b: fffffff7

隐式类型转换

  • 算术运算式中,低类型能够转换为高类型
  • 赋值运算式
    • 右边表达式的值自动隐式转换为左边变量的类型,并赋值给他
  • 函数调用中参数传递时,系统隐式地将实参转换为形参的类型后,赋给 形参
  • 函数有返回值时,系统将隐式地将返回表达式类型转换为返回值类型,赋值给调用函数

函数

静态函数: 不可被其他文件调用的函数

函数重载:

  • 参数类型不同
  • 参数个数不同
  • 对返回类型没有要求

八、#define & const & sizeof

#define实例

 #define SEC (60 * 60 * 24 * 365)UL
 #define MIIN(A, B)    ( (A) <= (B) ? (A) : (B) )

const,#define的区别

  • const
    • 有数据类型
    • 可进行类型安全检查
    • 可对其进行调试
  • #define
    • 没有数据类型
    • 仅进行字符替换,没有类型安全检查
    • 无法调试
  • c中const
    • 被当做一个不能被改变的普通变量
  • error
const bufsize = 100;
char buf[bufsize];

字节对齐

  • 数据对齐规则
    • 结构的首地址必须是结构内最宽类型的整数倍地址
    • 结构体的每一个成员起始地址必须是自身类型大小的整数倍
  • 结构体的整体大小必须可被对齐值整除
  • 结构体的整体大小必须可被本结构内的最宽类型整除

sizeof

  • 结构体或类内的静态变量
struct s{
  int a;
  static int b;
};
s ss;
sizeof(ss)
结果:4
  • 静态变量存放在全局数据区
  • sizeof**计算栈中**分配的大小
  • 任何类型指针大小相同:4(32位)
  • 对函数使用sizeof
  • 在编译阶段会被函数返回值的类型取代
  • 空类大小
    • 单继承:1
    • 多继承:1
    • 虚继承:4
    • 涉及虚表(虚指针)

说明:
1、为何空类的大小不是0呢?

为了确保两个不同对象的地址不同,必须如此。

类的实例化是在内存中分配一块地址,每个实例在内存中都有独一无二的二地址。同样,空类也会实例化,所以编译器会给空类隐含的添加一个字节,这样空类实例化后就有独一无二的地址了。所以,空类的sizeof为1,而不是0.

2、请看下面的类:

class A{ virtual void f(){} };

class B:public A{}

此时,类A和类B都不是空类,其sizeof都是4,因为它们都具有虚函数表的地址。

3、请看:

class A{};

class B:public virtual A{};

此时,A是空类,其大小为1;B不是空类,其大小为4.因为含有指向虚基类的指针。

4、多重继承的空类的大小也是1.

class Father1{}; class Father2{};

class Child:Father1, Father2{};

它们的sizeof都是1.

5、何时共享虚函数地址表:

如果派生类继承的第一个是基类,且该基类定义了虚函数地址表,则派生类就共享该表首址占用的存储单元。对于除前述情形以外的其他任何情形,派生类在处理完所有基类或虚基类后,根据派生类是否建立了虚函数地址表,确定是否为该表首址分配存储单元。


内联函数 vs. 宏

  • 内联
    • 相比普通函数: 加快程序运行速度
    • 直接嵌入目标代码
    • 要做参数类型检查

    • 简单的替换
    • 不做参数类型检查

九、 C++面向对象

1、类和对象

类对象的存储空间

  • 只为每个对象的数据成员和函数地址分配内存空间
  • 类中所有成员函数只生成一个副本
  • 该类每个对象执行相同的函数成员

拷贝构造函数

  • 功能
    • 用一个已知的对象来初始化一个被创建的同类的对象
  • 特点
    • 函数只有一个参数,并且是对某个对象的引用
    • 每个类都必须有一个拷贝初始化构造函数
  • 格式
    • 类名::拷贝初始化构造函数名(const 类名 &引用名)

静态成员

  • 静态数据成员
    • 特点
      类的所有对象共享
      必须初始化,且要在类外初始化
    • 引用格式
      类名::静态数据成员名
  • 静态成员函数
    • 特点
      类的所有对象共享
      只能使用类的静态成员和非数据成员
    • 引用格式
      类名::静态成员函数名

类成员指针

const成员函数

定义: 任何不修改成员数据的函数都应声明为const函数

原型: int GetY() const;

细节:

  • const函数想修改成员变量
    • 在相应变量定义处加上mutable
    • mutable int m_Count;

2、友元函数

定义

  • 需在类体内声明
  • 可访问类的私有成员
  • 不是类的成员函数

优点: 提高程序运行效率

缺点:破坏类的封装性和隐藏性

特点: 可以是多个类的友元

3、继承和派生

公有继承

  • 派生类成员函数可访问基类中的公有成员和保护成员
  • 派生类的对象仅可访问基类中的公有成员

派生类

  • 构造函数执行顺序
    • 基类构造函数
    • 子对象类的构造函数(如果有的话)
    • 派生类构造函数
  • 析构函数执行顺序
    • 派生类的析构函数
    • 基类的析构函数

虚基类

C++中new与malloc的10点区别

C++编译期多态与运行期多态

前言

今日的C++不再是个单纯的“带类的C”语言,它已经发展成为一个多种次语言所组成的语言集合,其中泛型编程与基于它的STL是C++发展中最为出彩的那部分。在面向对象C++编程中,多态是OO三大特性之一,这种多态称为运行期多态,也称为动态多态;在泛型编程中,多态基于template(模板)的具现化与函数的重载解析,这种多态在编译期进行,因此称为编译期多态或静态多态。在本文中,我们将了解:

  • 什么是运行期多态
  • 什么是编译期多态
  • 它们的优缺点在哪

运行期多态

运行期多态的设计思想要归结到类继承体系的设计上去。对于有相关功能的对象集合,我们总希望能够抽象出它们共有的功能集合,在基类中将这些功能声明为虚接口(虚函数),然后由子类继承基类去重写这些虚接口,以实现子类特有的具体功能。典型地我们会举下面这个例子:

技术分享

class Animal
{
public :
    virtual void shout() = 0;
};
class Dog :public Animal
{
public:
    virtual void shout(){ cout << "汪汪!"<<endl; }
};
class Cat :public Animal
{
public:
    virtual void shout(){ cout << "喵喵~"<<endl; }
};
class Bird : public Animal
{
public:
    virtual void shout(){ cout << "叽喳!"<<endl; }
};

int main()
{
    Animal * anim1 = new Dog;
    Animal * anim2 = new Cat;
    Animal * anim3 = new Bird;

   //藉由指针(或引用)调用的接口,在运行期确定指针(或引用)所指对象的真正类型,调用该类型对应的接口
    anim1->shout();
    anim2->shout();
    anim3->shout();

    //delete 对象
    ...
   return 0;
}

运行期多态的实现依赖于虚函数机制。当某个类声明了虚函数时,编译器将为该类对象安插一个虚函数表指针,并为该类设置一张唯一的虚函数表,虚函数表中存放的是该类虚函数地址。运行期间通过虚函数表指针与虚函数表去确定该类虚函数的真正实现。

运行期多态的优势还在于它使处理异质对象集合称为可能:

//我们有个动物园,里面有一堆动物
int main()
{
    vector<Animal*>anims;

    Animal * anim1 = new Dog;
    Animal * anim2 = new Cat;
    Animal * anim3 = new Bird;
    Animal * anim4 = new Dog;
    Animal * anim5 = new Cat;
    Animal * anim6 = new Bird;

    //处理异质类集合
    anims.push_back(anim1);
    anims.push_back(anim2);
    anims.push_back(anim3);
    anims.push_back(anim4);
    anims.push_back(anim5);
    anims.push_back(anim6);

    for (auto & i : anims)
    {
        i->shout();
    }
    //delete对象
    //...
    return 0;
}

总结:运行期多态通过虚函数发生于运行期

编译期多态

对模板参数而言,多态是通过模板具现化和函数重载解析实现的。以不同的模板参数具现化导致调用不同的函数,这就是所谓的编译期多态。
相比较于运行期多态,实现编译期多态的类之间并不需要成为一个继承体系,它们之间可以没有什么关系,但约束是它们都有相同的隐式接口。我们将上面的例子改写为:

class Animal
{
public :
    void shout() { cout << "发出动物的叫声" << endl; };
};
class Dog
{
public:
     void shout(){ cout << "汪汪!"<<endl; }
};
class Cat
{
public:
     void shout(){ cout << "喵喵~"<<endl; }
};
class Bird
{
public:
     void shout(){ cout << "叽喳!"<<endl; }
};
template <typename T>
void  animalShout(T & t)
{
    t.shout();
}
int main()
{
    Animal anim;
    Dog dog;
    Cat cat;
    Bird bird;

    animalShout(anim);
    animalShout(dog);
    animalShout(cat);
    animalShout(bird);

    getchar();
}

在编译之前,函数模板中t.shout()调用的是哪个接口并不确定。在编译期间,编译器推断出模板参数,因此确定调用的shout是哪个具体类型的接口。不同的推断结果调用不同的函数,这就是编译器多态。这类似于重载函数在编译器进行推导,以确定哪一个函数被调用。

运行期多态与编译期多态优缺点分析

运行期多态优点

  • OO设计中重要的特性,对客观世界直觉认识。
  • 能够处理同一个继承体系下的异质类集合。

运行期多态缺点

  • 运行期间进行虚函数绑定,提高了程序运行开销。
  • 庞大的类继承层次,对接口的修改易影响类继承层次。
  • 由于虚函数在运行期在确定,所以编译器无法对虚函数进行优化。
  • 虚表指针增大了对象体积,类也多了一张虚函数表,当然,这是理所应 - 当值得付出的资源消耗,列为缺点有点勉强。

编译期多态优点

  • 它带来了泛型编程的概念,使得C++拥有泛型编程与STL这样的强大武器。
  • 在编译器完成多态,提高运行期效率。
  • 具有很强的适配性与松耦合性,对于特殊类型可由模板偏特化、全特化来处理。

编译期多态缺点

  • 程序可读性降低,代码调试带来困难。
  • 无法实现模板的分离编译,当工程很大时,编译时间不可小觑。
  • 无法处理异质对象集合。

关于显式接口与隐式接口

所谓的显式接口是指类继承层次中定义的接口或是某个具体类提供的接口,总而言之,我们能够在源代码中找到这个接口.显式接口以函数签名为中心,例如

void AnimalShot(Animal & anim)
{
    anim.shout();
}

我们称shout为一个显式接口。在运行期多态中的接口皆为显式接口。

而对模板参数而言,接口是隐式的,奠基于有效表达式。例如:

template <typename T>
void AnimalShot(T & anim)
{
    anim.shout();
}

对于anim来说,必须支持哪一种接口,要由模板参数执行于anim身上的操作来决定,在上面这个例子中,T必须支持shout()操作,那么shout就是T的一个隐式接口。

C++ 字符串使用详解

如何使用C++实现一个简单的集合类

C++ 内存分配(new,operator new)详解

编程风格

<script type="text/javascript"> $(function () { $(‘pre.prettyprint code‘).each(function () { var lines = $(this).text().split(‘\n‘).length; var $numbering = $(‘
    ‘).addClass(‘pre-numbering‘).hide(); $(this).addClass(‘has-numbering‘).parent().append($numbering); for (i = 1; i <= lines; i++) { $numbering.append($(‘
  • ‘).text(i)); }; $numbering.fadeIn(1700); }); }); </script>

    CPP 学习笔记随笔