一、基础知识

1、C++编译流程

以Unix系统编译中间文件为说明:

.cpp—(编译预处理)—>.ii—(编译)—>.s—(汇编)—>.o—(ld,连接)—>.out

2、#include

作用于编译预处理阶段,将被include文件抄送在include所在位置,并会在相应位置写出调用栈,生成中间文件.ii,该中间文件可读

include文件加引号表示先从当前目录寻找索引,加尖括号表示从编译器指定根目录索引,Unix默认为"~//usr/include"目录

3、定义、声明、头文件

.h头文件中只应存放三种代码:

函数声明:没有大括号,形如void fun()

变量声明:extern 变量名

class、结构体定义

extern表示声明一个全局变量

声明只是提示编译器,存在这个东西,并没有定义出实体,不定义直接调用会报错。

4、标准头文件结构

#ifndef HEADER_FLAG

#define HEADER_FLAG

/*头文件*/

#endif

这是为了防止多次include同一个头文件时,每次都抄送到预编译文件中,造成文件过大、循环导入或者结构体定义重复以致报错(声明重复问题不大)。

5、默认参数

在声明中写默认参数,不在定义中给默认参数。

6、调用函数过程

本地变量进入堆栈(未必初始化)

函数参数进入堆栈

返回地址计入堆栈

返回值进入寄存器(运行函数)

pop掉参数

返回值进入堆栈(返回地址,所以要pop掉参数,堆栈先进后出)

7、内联函数

在编译阶段优化,省略上一小结中复杂的堆栈操作,效果如下,

汇编(伪)优化如下,

注意,inline 函数名实际是一个声明,而非定义所以不需要额外声明。实际上以空间换时间(编译会将函数插入调用位置),编译器如果发现函数递归或者过于巨大,可能会拒绝inline操作,函数较小可能被自动inline,建议就是小函数inline(2-3行),超过20行的就不要inline了。

相比于宏,inline可以做类型检查,给出debug提示,下图中C++会提示double的f(a)和%d不匹配,C会直接给出一个奇怪的值,

8、const

初始化之后不可修改,值得注意的是下图这种,指针和const,到底是地址(指针)还是地址中的内容(对象)是const

重点在于const和*的位置顺序,下述代码中2、3两句等价,

且const变量不能传给其他非const的指针(因为这样有可能造成修改),

函数和const

函数虚参加const表示函数内部不可修改该变量,对输入无要求

return const 对接收函数返回的类型无要求

class和const

const 对象,此时我们不能保证class方法是否修改成员变量,又不能限制函数不能使用(class就没有意义了)

const 对象 or 成员变量是const,要求成员变量必须有初始值,因为事后无法赋值

main文件(编译时可以感知类声明文件),类声明文件,类定义文件:声明、定义(两个位置都需要添加)函数时后面添加const关键字如,int fun() const。

下图运行结果为"f() const",

实际构成了重载,

void f(A* this)

void f(const A* this)

9、字符串

char *s = "Hello World";  // 将代码段的字符串地址直接付给指针,所以后面尝试修改会报错(代码段不可修改),

             // 应该在开头改写为const char *s

char s[] = "Hello World";  // 数组被写入堆栈,将代码段的字符串拷贝到堆栈

10、引用

char& r = c;  // 引用可以做左值

相当于给c取了一个别名,此时c、r绑定到同一实体。

int x;int y;

int& a = x;

int& b = y;

a = b;  // 等价x=y

注意,引用无法取地址,即 int&* r 的写法是错误的,不过相对的,int*& p 是没问题的,指针可以被引用。

class的成员变量是引用时

此时只能使用initializer list的方式初始化引用对应的变量,如果在{}中使用m_y=a则表示将a复制给m_y对应的变量。

函数返回引用时

return一个全局变量,

这个引用表示变量,不表示值,所以最后一句表示赋值。

11、中间结果

相当于Python中的“_”,i*3这样的结果会作为const int类型临时保存。

二、class入门

1、变量

Field,成员变量,作用域为class的对象,类的函数中可以直接使用;class本身不能拥有变量,理解为声明一个变量(函数和变量不同,函数属于class而不是对象);

parameters,函数参数;

local variables,本地变量,作用域为本函数;

后两者完全相同,本地存储,出来作用域则不存在该变量。

关键字 this:一个指针,为当前对象的指针(指该次调用成员函数的对象的指针),

经由指针this区分调用成员函数的不同实例,其原理如下:使用'实例对象.成员函数'来调用等价于直接调用该函数并将对象指针作为首个参数输入,即:成员函数(this),原理和python一致,成员函数实际上有一个默认存在的参数输入,接收实例指针

2、构造和析构

在C++中,class实例化时成员变量不会初始化,仅仅寻找到一块足够大的地址(java会清空地址内数据)。VS会在debug时为未初始化空间填充0xcd,用于排查(0xcd0xcd在国标码中为‘烫’)。

constructor:构造函数,初始化对象时自动执行(相当于python中的__init__)

函数名和class名相同

没有返回类型

destructor:析构函数,退出对象所在scope时自动执行

函数名为'~'加构造函数名

没有返回类型

不可以有参数

有关‘{}’,表示scope,如下代码中,进入‘{’后会执行Tree的构造函数,退出‘}’前,本scope内资源回收,会自动执行析构函数

数组、结构体、使用构建函数的class初始化方式对比:

Y经由构建函数Y()间接将f、i赋值,顺便一提,数组b后面未指定元素会被初始化为0

default constructor:无参数构建函数,见下右的第二行会报错,因为构建y2有两个元素,而第二个元素会调用default constructor,但实际上constructor需要参数,所以会报错:

:

3、scope和存储空间

编译器在‘{}’开始的位置会分配好空间,而在运行到相关定义时才会真正的运行构造函数。

如下图,某些情况下编译会出错,因为一旦goto成功,则x1不会被构建,相应的退出‘{}’时,析构函数执行会失败。

4、动态分配空间

new:制造对象,类似malloc;分配空间、调用构造函数(对于class),返回地址

使用一张表,记录下每次申请的内存大小和对应的地址

delete:收回空间,类似free;析构对象(对于class)、回收空间;它有两种用法,如下:

delete p :普通用法

delete[] p :一般来说new p[]时,需要使用这个,会将所有对象的析构函数分别调用,否则回收内存正常,但只调用指针直接指向的对象的析构

5、访问控制

public:任何人可以访问

private:成员函数可以访问 ,注意对class来讲,同一个class不同对象可以互相访问私有变量,如下代码,p[0]是可以访问b的私有变量的

friends:声名一个函数/class等,使之可以访问自己(本class的任何实例)的私有变量

下面代码涉及两个知识点:1、friends声明在class内部;2、结构体可以前向声明(开头的X),用于在结构体Y定义中占位。

protected:自己及子类可以访问

6、struct vs class

未指定访问控制属性的变量、函数,class默认为private,struct默认public

7、初始化list

初始化后才执行构造函数(大括号中语句)

在大括号中赋值的话会先默认初始化变量,然后赋值;初始化list的方式直接用目标值初始化变量

8、成员函数和inline

在class内部给出了body的成员函数,视为内联函数。

三、父类子类

1、组合和继承

组合:已有类作为新的类的成员

继承:改造类,class B: public A {},意为B类为A类子类

     父类的private,在子类中存在,但是不能直接访问(需要使用父类的public方法),需要使用protected声明。

另一点值得注意的是,由于构造函数不可以直接调用, 调用父类的构造函数方式需要使用初始化list方法,而且必须最先构造父类(如果父类构造函数有参数),构造先父后子,析构先子后父:

2、覆盖(override)、重载(overload)、隐藏

overload

在同一作用域中,函数名相同,参数列表不同,返回值可同可不同的函数,编译器会根据传入参数决定调用哪个函数,注意仅返回值不同不能构成overload关系。

override

又叫覆盖,是指不在同一个作用域中(分别在父类和子类中),函数名,参数个数,参数类型,返回值类型都相同,并且父类函数必须有virtual关键字的函数,就构成了重写(协变除外)。协变:协变也是一种重写,只是父类和子类中的函数的返回值不同,父类的函数返回父类的指针或者引用,子类函数返回子类的指针或者引用。

virtual:子类的同名同参函数之间有联系(继承树中某一个函数是virtual的,子类的该方法都是virtual的)。

重定义

又叫隐藏,是指在不同的作用域中(分别在父类和子类中),函数名相同,不能构成重写的都是重定义(重定义的不光是函数,还可以是成员变量),隐藏和覆盖不同,被隐藏的父类成员可以通过子类.父类::成员的方式调用。

3、向上造型upcasting

子类对象可以被传给父类对象指针,如下图所示,

这是由于C++的class类似于C的结构体,实际上是一个指针指向一块有特定内容排列顺序的内存,子类只会在父类的内存规划上向后扩充,不会更改父类已经规划好的部分。如果有子类方法隐藏了父类方法,向上造型后会隐藏失效,此时的对象指针仅能识别父类原有的模块。

类似地,也有向下造型,不过可能会出错。

Employee是Manager的父类

4、多态

本小节摘抄自文章:C++ 多态的实现及原理

想要理解多态,需要区分函数和虚函数的区别(内存上的位置差异),并要理解向上造型的概念,了解了前面两点,就了解了动态绑定、静态绑定的区别,对于多态产生的种种现象就能够从机理上给出自己的解释。

virtual虚函数内存机制

上面提到过,virtual是让子类与父类之间的同名函数有联系,这就是多态性,实现动态绑定。

任何类若是有虚函数就会比正常类大一点,所有有virtual的类的对象里面最头上会自动加上一个隐藏的,不让我知道的指针,它指向一张表,注意,该表对于同一个class的不同对象是同一个,不同class(指父类子类)的表不同。这张表叫做vtable(虚表),vtable里是所有virtual函数的地址,对于下面代码,

class Shape {
public:
Shape();
virtual ~Shape();
virtual void render();
void move(const pos&);
virtual void resize();
protected:
pos center;
};

其内存分布如下:

我们看一下其子类的内存分布:

class Ellipse : public Shape{
public:
Ellipse (float majr, float minr);
virtual void render(); protected:
float major_axis;
float minor_axis;
};

这里的resize沿用了shape的成员函数。

多态实现逻辑

看如下代码,

#include "stdafx.h"
#include <iostream>
#include <stdlib.h>
using namespace std; class Father
{
public:
void Face()
{
cout << "Father's face" << endl;
} void Say()
{
cout << "Father say hello" << endl;
}
}; class Son:public Father
{
public:
void Say()
{
cout << "Son say hello" << endl;
}
}; void main()
{
Son son;
Father *pFather=&son; // 隐式类型转换
pFather->Say();
}

输出的结果为:

我们在main()函数中首先定义了一个Son类的对象son,接着定义了一个指向Father类的指针变量pFather,然后利用该变量调用pFather->Say().估计很多人往往将这种情况和c++的多态性搞混淆,认为son实际上是Son类的对象,应该是调用Son类的Say,输出"Son
say hello",然而结果却不是.

从编译的角度来看:

c++编译器在编译的时候,要确定每个对象调用的函数(非虚函数)的地址,这称为早期绑定,当我们将Son类的对象son的地址赋给pFather时,c++编译器进行了类型转换,此时c++编译器认为变量pFather保存的就是Father对象的地址,当在main函数中执行pFather->Say(),调用的当然就是Father对象的Say函数

从内存角度看:

    

Son类对象的内存模型如上图

我们构造Son类的对象时,首先要调用Father类的构造函数去构造Father类的对象,然后才调用Son类的构造函数完成自身部分的构造,从而拼接出一个完整的Son类对象。当我们将Son类对象转换为Father类型时,该对象就被认为是原对象整个内存模型的上半部分,也就是上图中“Father的对象所占内存”,那么当我们利用类型转换后的对象指针去调用它的方法时,当然也就是调用它所在的内存中的方法,因此,输出“Father
Say hello”,也就顺理成章了。

正如很多人那么认为,在上面的代码中,我们知道pFather实际上指向的是Son类的对象,我们希望输出的结果是son类的Say方法,那么想到达到这种结果,就要用到虚函数了。

前面输出的结果是因为编译器在编译的时候,就已经确定了对象调用的函数的地址,要解决这个问题就要使用晚绑定,当编译器使用晚绑定时候,就会在运行时再去确定对象的类型以及正确的调用函数,而要让编译器采用晚绑定,就要在基类中声明函数时使用virtual关键字,这样的函数我们就称之为虚函数,一旦某个函数在基类中声明为virtual,那么在所有的派生类中该函数都是virtual,而不需要再显式地声明为virtual。

代码稍微改动一下,看一下运行结果

#include "stdafx.h"
#include <iostream>
#include <stdlib.h>
using namespace std; class Father
{
public:
void Face()
{
cout << "Father's face" << endl;
} virtual void Say()
{
cout << "Father say hello" << endl;
}
}; class Son:public Father
{
public:
void Say()
{
cout << "Son say hello" << endl;
}
}; void main()
{
Son son;
Father *pFather=&son; // 隐式类型转换
pFather->Say();
}

我们发现结果是"Son say hello"也就是根据对象的类型调用了正确的函数,那么当我们将Say()声明为virtual时,背后发生了什么。

编译器在编译的时候,发现Father类中有虚函数,此时编译器会为每个包含虚函数的类创建一个虚表(即 vtable),该表是一个一维数组,在这个数组中存放每个虚函数的地址,

那么如何定位虚表呢?编译器另外还为每个对象提供了一个虚表指针(即vptr),这个指针指向了对象所属类的虚表,在程序运行时,根据对象的类型去初始化vptr,从而让vptr正确的指向了所属类的虚表,从而在调用虚函数的时候,能够找到正确的函数,对于第二段代码程序,由于pFather实际指向的对象类型是Son,因此vptr指向的Son类的vtable,当调用pFather->Son()时,根据虚表中的函数地址找到的就是Son类的Say()函数.

正是由于每个对象调用的虚函数都是通过虚表指针来索引的,也就决定了虚表指针的正确初始化是非常重要的,换句话说,在虚表指针没有正确初始化之前,我们不能够去调用虚函数,那么虚表指针是在什么时候,或者什么地方初始化呢?

答案是在构造函数中进行虚表的创建和虚表指针的初始化,在构造子类对象时,要先调用父类的构造函数,此时编译器只“看到了”父类,并不知道后面是否还有继承者,它初始化父类对象的虚表指针,该虚表指针指向父类的虚表,当执行子类的构造函数时,子类对象的虚表指针被初始化,指向自身的虚表。

『C++』基础知识点的更多相关文章

  1. 『TensorFlow』专题汇总

    TensorFlow:官方文档 TensorFlow:项目地址 本篇列出文章对于全零新手不太合适,可以尝试TensorFlow入门系列博客,搭配其他资料进行学习. Keras使用tf.Session训 ...

  2. 『PyTorch』第十弹_循环神经网络

    RNN基础: 『cs231n』作业3问题1选讲_通过代码理解RNN&图像标注训练 TensorFlow RNN: 『TensotFlow』基础RNN网络分类问题 『TensotFlow』基础R ...

  3. 『TensotFlow』RNN/LSTM古诗生成

    往期RNN相关工程实践文章 『TensotFlow』基础RNN网络分类问题 『TensotFlow』RNN中文文本_上 『TensotFlow』基础RNN网络回归问题 『TensotFlow』RNN中 ...

  4. 关于『HTML』:第一弹

    关于『HTML』:第一弹 建议缩放90%食用 根据C2024XSC212童鞋的提问, 我准备写一稿关于『HTML』基础的帖 But! 当我看到了C2024XSC130的 "关于『HTML5』 ...

  5. 『cs231n』计算机视觉基础

    线性分类器损失函数明细: 『cs231n』线性分类器损失函数 最优化Optimiz部分代码: 1.随机搜索 bestloss = float('inf') # 无穷大 for num in range ...

  6. 关于『Markdown』:第一弹

    关于『Markdown』:第一弹 建议缩放90%食用 声明: 在我之前已有数位大佬发布 "Markdown" 的语法知识点, 在此, 仅整理归类以及补缺, 方便阅读. 感谢 C20 ...

  7. [原创] 【2014.12.02更新网盘链接】基于EasySysprep4.1的 Windows 7 x86/x64 『视频』封装

    [原创] [2014.12.02更新网盘链接]基于EasySysprep4.1的 Windows 7 x86/x64 『视频』封装 joinlidong 发表于 2014-11-29 14:25:50 ...

  8. 『TensorFlow』批处理类

    『教程』Batch Normalization 层介绍 基础知识 下面有莫凡的对于批处理的解释: fc_mean,fc_var = tf.nn.moments( Wx_plus_b, axes=[0] ...

  9. 『TensorFlow』梯度优化相关

    tf.trainable_variables可以得到整个模型中所有trainable=True的Variable,也是自由处理梯度的基础 基础梯度操作方法: tf.gradients 用来计算导数.该 ...

随机推荐

  1. 一个查表置换的CM

    说实话,今天被自己蠢哭了 因为看多了一个字符,以为是输入字符变形后的base64编码,也怪自己没大致看过base64汇编形式,把base64跟完了用py实现完算法才意思到是base64,这是题外话 本 ...

  2. SQL Server (MSSQLSERVER) 服务因 2148081668 服务性错误而停止。

    https://zhidao.baidu.com/question/151448005.html 具体步骤: 运行-> 输入:“services.msc” ->找到 “SQL Server ...

  3. P2604 [ZJOI2010]网络扩容

    思路 简单的费用流问题,跑出第一问后在残量网络上加边求最小费用即可 代码 #include <cstdio> #include <algorithm> #include < ...

  4. Google advertiser api开发概述——部分失败

    部分失败 某些 AdWords 服务允许您请求执行有效操作,而对失败的操作返回错误.此功能(称为部分失败)允许您在结束时单独处理失败的操作. 技术细节 要使用此功能,您需要设置此可选的 SOAP 标头 ...

  5. keySet,entrySet用法 以及遍历map的用法

    Set<K> keySet() //返回值是个只存放key值的Set集合(集合中无序存放的)Set<Map.Entry<K,V>> entrySet() //返回映 ...

  6. windows特殊文件或文件夹

    考了很多文章,搜集了很多资料整理而成.好的用途可以用来隐藏个人资料,防止误删,病毒免疫等等.至于坏的方面,当然也可用来隐藏木马等等,就看你怎么用了.还有一个没有搞明白,资料上也没找到,请知道的指点一下 ...

  7. QT移植无法启动 This application failed to start because it could not find or load the QT platform

    QT配置好在自己机器上可以运行,但在别人机器上一直弹出 "This application failed to start because it could not find or load ...

  8. Winform 设置控件值

    private void SetControlValue(Control control, object value) { try { control.FindForm().Invoke((Actio ...

  9. 用Let's Encrypt实现Https(Windows环境+Tomcat+Java)

    补充1: 已解决20的部分问题,移步这里 单域名下多子域名同时认证HTTPS 补充2: 之前忘了说了,我这个方法只对Tomcat7.0以上有用(要不然就是8.0...) 我自己用的是9.0 原因好像是 ...

  10. Python 学习笔记 多进程 multiprocessing--转载

    本文链接地址 http://quqiuzhu.com/2016/python-multiprocessing/ Python 解释器有一个全局解释器锁(PIL),导致每个 Python 进程中最多同时 ...