C++基础教程

这篇博客详细介绍了C++的基础知识,包括C++编译流程、头文件、定义和声明、内联函数、const、引用、字符串、类入门、父类子类等概念,深入解析了类的构造、析构、成员函数、继承、多态等核心特性。

​​​​​一、基础知识

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函数的地址,对于下面代码,

1

2

3

4

5

6

7

8

9

10

class Shape {

public:

    Shape();

    virtual  ~Shape();

    virtual void render();

    void move(const pos&);

    virtual void resize();

protected:

    pos center;

};

 其内存分布如下:

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

1

2

3

4

5

6

7

8

9

class Ellipse : public Shape{

public:

    Ellipse (float majr, float minr);

    virtual void render();

protected:

    float major_axis;

    float minor_axis;

};

 

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

多态实现逻辑

看如下代码,

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

31

32

33

34

35

#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。

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

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

31

32

33

34

35

#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()函数.

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

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

目录 1. 简介 怎样使用本教程 2. C++基础 Basics of C++ 1. C++程序结构 Structure of a program 2. 变量和数据类型 Variables and Data types 3. 常量 Constants 4. 操作符/运算符 Operators 5. 控制台交互 Communication through console 3. 控制结构和函数 Control structures and Functions 1. 控制结构 Control Structures 2. 函数I Functions I 3. 函数II Functions II 4. 高级数据类型 Advanced Data 1. 数组 Arrays 2. 字符序列 Character Sequences 3. 指针 Pointers 4. 动态内存分配 Dynamic memory 5. 数据结构 Data Structures 6. 自定义数据类型 User defined data types 5. 面向对象编程 Object-oriented Programming 1. 类,构造函数和析构函数,类的指针 Classes. Constructors and Destructors. Pointers to classes. 2. 操作符重载,this,静态成员 Overloading Operators. this. Static members 3. 类之间的关系 Relationships between classes: friend. Inheritance 4. 虚拟成员,抽象,多态 Virtual Members. Abstraction. Polymorphism 6. C++高级 Advanced concepts 1. 模板 Templates 2. 名空间 Namespaces 3. 出错处理 Exception handling 4. 类型转换高级 Advacned Class Type-casting 5. 预处理指令 Preprocessor Directives 7. C++ 标准函数库 C++ Standard Library 1. 文件的输入输出 Input/Output with files C++基础教程简介 怎样使用本教程 读者范围 本教程面向所有希望学习C++语言的读者。如果读者有其他编程语言背景或计算机相关基本知识可以帮助更好的理解教程内容,但这并非必须条件。 对于C语言熟悉的读者可将前三章(1.1 到 3.4)当作复习,因为这部分内容主要介绍C++中的C部分。不过某些C++的语法与C还是有些差别,所以建议还是快速的读一下这部分。 第四章讲述面向对象编程。 第五章主要介绍ANSI-C++标准中的新增的功能。 本教程结构 教程共分6章,每章分若干小节。你可以直接从主目录进入任意小节,并循每页底部的链接向后浏览。 很多小节含有一页例题介绍该章节主要知识点的使用。建议在进入下一章学习之前最好先阅读这些例题,理解每行代码。 学习和练习一种编程语言的最好办法是自己修改书中例题程序,设法在程序中增加新的功能。不要不敢修改这些例题程序,这正是学习的方法。 兼容性备注 ANSI-C++标准近几年来被接受为国际标准。尽管C++语言从二十世纪80年代即存在,ANSI-C++在1997年才被发表,2003年又被修订过。因此很多编译器不支持ANSI-C++中的部分新功能,特别是那些在此标准发表前即被发布的编译器。 在本教程中,那些ANSI-C++中新增的而老一代C++编译器大多不支持概念将备用如下标志标出: ANSI C++新增的概念 同样对于C和C++在实现上有明显不同的概念,将备用如下标志标出: C 与 C++不同的地方 编译器 本教程中所有例题程序均为console程序(控制台程序)。此类程序以文本形式与用户交换信息,显示结果。 所有C++编译器均支持console程序的编译。要了解更多关于如何编译的说明,请查询你的编译器用户使用手册。 C++编译器和开发环境推荐 很多读者询问编译器和开发环境的问题。除了常用的商用收费的MS Visual Studio, VC++,Borland C++等工具外,还有很多免费的工具也是很好用的。这里推荐两种免费的C++开发软件: 1、Eclipse的CDT开发工具,官方网站在http://www.eclipse.org/cdt/ 2、开源工具Dev-C++和wxDev-C++
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值