c++基础
大学的遗物
从最开始基础开始,一直说到c++面向对象,主要参考菜鸟教程及网上博客
c++简介
c++特性
封装、抽象、继承、多态
c++数据类型
各种类型的存储大小与系统位数有关
可以使用 typedef 为一个已有的类型取一个新的名字: typedef type newname;
枚举类型enum是c++中的一种派生数据类型,用于枚举用户定义的若干常量的集合,表示一个变量的几种可能的值。默认情况下,枚举值从0开始递增,且可以自定义值大小。
c++的变量类型
每个变量都有指定的类型,类型决定了变量存储的大小和布局。
可以使用 extern 关键字在任何地方声明一个变量。
需要分清变量的声明、定义和初始化。
左值(lvalue):指向内存位置的表达式被称为左值表达式
右值(rvalue):术语右值指的是存储在内存中某些地址的数值。右值是不能对其进行赋值的表达式,也就是说,右值可以出现在赋值号的右边,但不能出现在赋值号的左边。
c++的变量作用域
局部变量(函数或代码块中声明的变量)、形式参数(函数参数中定义的变量)、全局变量(在所有函数外部声明的变量)
局部变量被定义时,系统不会对其进行初始化。全局变量定义时,系统回有一个默认的初始化(通常时全零)。
c++的常量
常量就是固定值,也成为字面量。常量可以是任何基本数据类型,则整型、浮点值、字符、字符串和布尔值。
常见的前缀后缀如下所示:
| 进制 | 前缀 |
|---|---|
| 八进制 | 0 |
| 十进制 | 无 |
| 十六进制 | 0x或者0X |
| 后缀 | 含义 |
|---|---|
| u或U | 无符号整数 |
| l或L | 长整数 |
浮点常量可以有小数形式和科学计数法形式。
布尔常量只有true和false,不应该true看成1,把false看成0。
字符常量常常扩在单引号中,\加上某些符号会形成转义字符。
字符串常量常常扩在双引号中,可以包含普通字符、转义字符和通用字符。
定义常量有两种方式:#define预处理器和const关键字。常量定义为大写字母形式,是一个很好的编程实践。
c++的修饰符
signed、unsigned、long、short
c++的函数
每个c++程序都至少有一个函数,即主函数main()。
函数组成部分:
- 返回类型
- 函数名称
- 参数
- 函数主体
函数声明:告诉编译器函数名称以及调用函数的方式。当在一个源文件中定义且在另一个文件中调用函数,函数声明是必须的。这种情况下,应该在调用函数的文件顶部声明函数。
函数调用:调用函数时,需要传递所需的参数。函数接受参数时,应该首先声明参数值的变量,这些变量称为形式参数。形参就像函数内的其它局部变量,在进入函数时被创建,退出函数时被销毁。函数调用有以下三种向函数传递参数的方式:
- 传值调用:该方法把实际的值赋给函数形参。这种情况下,修改函数内的形参对实参没有影响
- 指针调用:该方法把参数的地址赋值给形参。在函数中,该地址用于访问调用中要用到的实参。这意味着,修改形参会改变实参。
- 引用调用:该方法的参数的引用赋值给形参。在函数内,该引用用于访问调用中要用到的实参。这意味着,修改形参回改变实参。
默认情况下,c++用传值调用来传递参数,这意味着函数内的代码不能改变用于调用函数的参数。
当定义函数时,可以在参数列表后指定参数默认值。当调用函数时,如果实参的值留空,则使用这个默认值。
lambda函数与表达式
c11对匿名函数提供支持,成为lambda函数。lambda函数把函数看成对象,lambda表达式可以像对象一样使用。比如可以将他们赋给变量作为参数传递,还可以向函数一样对其求值。
c++数组
存储一个固定大小的相同类型的顺序集合,特定元素可以通过索引的方式访问。
声明数组需要指定元素的类型和元素的数量。
指向数组的指针
使用数组名作为常量指针时合法的,反之亦然。
对以下程序:
1 | |
传递数组给函数
可以通过指定不带索引的数组名来传递一个指向数组的指针。
c++传数组给函数,数组类型自动转换成指针类型,因而传的实际是地址。
如果在函数中传递一个一维数组为参数,必须以以下三种方式来声明函数形参,且三种方式的结果一致,这是因为每种方式都会告诉编译器要接受一个指针。同样,也可以传递一个多为数组为形参。三种方式为:
1 | |
就函数而言,数组的长度是无关紧要的,因为c++不对形参执行边界检查。
从函数返回数组
c++不允许返回一个完整的数组作为参数的返回值。可以通过指定不带索引的数组名来返回一个指向数组的指针。如果要返回一个一维数组,必须声明一个返回指针的函数。如下
1 | |
另外,c++不支持函数外返回局部变量的地址(考虑程序调用的堆栈使用情况,会被回收),除非定义局部变量为static变量。(静态变量的位置是在静态区,会在程序最开始就初始地址,一般具有和程序相同的生命周期。而局部变量存放在堆栈,即是动态区)
c++字符串
c风格字符串
是使用null字符\0终止的一位字符数组。c++编译器会自动把\0 放在字符串的末尾,而不需要自己书写。
一些字符串操作函数:
1 | |
c++的string类
它是一个类,用类的思想封装了很多字符串操作。
c++指针
每个变量都有一个内存位置,每个内存位置都定义了可使用连字号**&**运算符访问的地址,这里又涉及到了字节对齐(为了使数据的存储和访问更加方便,采用字节对齐的方式,取为2的幂次地址)。
指针是一个变量,其值是一个地址。不同数据类型的指针之间唯一的不同是,指针所指向的变量或常量的数据类型不同。
一元运算符*****用来返回位于操作数所指定地址的变量的值。定义格式中指针变量前面的“*”,表示定义的变量的类型为指针型变量,“*”不是指针变量的一部分
如下将总结一些重要概念:
Null指针
在声明指针变量时,如果没有确切的地址可以赋值,为其赋值为Null是良好的习惯。指向Null的指针被成为空指针。
在大多数操作系统中,地址0的内存不允许被访问,被系统保留。同时,它具有非常重要的意义:表明指针不指向一个可以访问的内存地址。在程序中,要特别注意空指针的存在和使用。
指针的算术运算
指针本质是用数值表示地址,因而可以用算术运算对其操作,支持,++,–,+,-。
指针增减的字节数目,应该根据指针的类型来确定。对于指向int的指针加一,地址应该+4,而指向char的指针加一,地址应该+1。
数组是一个常量指针,因此在程序中常常可以用一个变量指针来访问数组,访问时递增即可。
指针也可以用关系运算符进行比较。
指针和数组
两者很多情况下可以相互替换的。但是也有特别的情况。
1 | |
把指针运算符 * 应用到 var 上是完全可以的,但修改 var 的值是非法的。这是因为 var 是一个指向数组开头的常量,不能作为左值。由于一个数组名对应一个指针常量,只要不改变数组的值,仍然可以用指针形式的表达式。
1 | |
指针数组
1 | |
ptr声明为数组,由5个int指针组成。每个元素,都是指向int值的指针。
指向指针的指针(多级间接寻址)

1 | |
同样的,访问这个值需要两个*。
这里只需要注意*和&两个符号的含义即可。*是表示指针和访问指针指向的值两个作用,&是将一个值本身存放的地址返回回来。
传递指针给函数及返回指针
传递指针也意味着可以传递数组作为参数。
注意,c++不支持返回局部变量的地址,除非定义为static变量(同上述返回数组)。
c++引用
引用变量是一个别名,它是一个已存在变量的另外一个名字。一旦把引用初始化为某个变量,就可以使用该引用名称或者变量名称来指向变量。
引用和指针的区别
- 不存在空引用,引用必须连接到一个合法的内存。
- 一旦引用被初始化为一个对象,就不能指向到另一个对象。指针可以在任何时候指向另一个对象。
- 引用必须在创建时初始化。指针可以在任何时候初始化。
可以用原始变量和引用来访问变量的内容。
1 | |
把引用作为参数和返回值
当返回一个引用时,引用的对象不能超出作用域,所以对一个局部变量引用是非法的。可以返回一个对于静态变量的引用。
1 | |
c++指针和引用混讲
一个数组不能用另一个数组初始化,也不能将一个数组赋值给另一个数组。解析
若指针保存0值,表明它不指向任何对象。但是把int型变量赋值给指针是非法的,尽管此int型变量的值可能为0,当然直接用int型变量给指针赋值也是非法的。
1 | |
1 | |
- cstr的类型是 string * const 还是 const string * ?
答:是string *const cstr,而非 const string *cstr。容易产生误解的原因是const限定符既可以放在类型前也可以放在类型后,const pstring cstr等价于pstring const cstr。遇到此类问题时,把const放在类型之后来理解。
区分:int *ip[4] 和 int (*ip)[4],第一个表示一个数组,元素是int指针;第二个表示一个指针,指向int数组,遇到此类问题时,由内向外读。解析
- 关于值传递,指针传递,引用传递
| 值传递 | 指针传递 | 引用传递 |
|---|---|---|
| 形参是实参的拷贝,改变形参的值并不会影响外部实参的值。从被调用函数的角度来说,值传递是单向的(实参->形参),参数的值只能传入,不能传出。当函数内部需要修改参数,并且不希望这个改变影响调用者时,采用值传递。 | 形参为指向实参地址的指针,当对形参的指向操作时,就相当于对实参本身进行的操作 | 形参相当于是实参的”别名”,对形参的操作其实就是对实参的操作,在引用传递过程中,被调函数的形式参数虽然也作为局部变量在栈中开辟了内存空间,但是这时存放的是由主调函数放进来的实参变量的地址。被调函数对形参的任何操作都被处理成间接寻址,即通过栈中存放的地址访问主调函数中的实参变量。正因为如此,被调函数对形参做的任何操作都影响了主调函数中的实参变量。 |
- 函数指针:指向函数的指针
函数指针的声明类似于函数的声明,只不过将函数名变成了 **(*指针名)**,例如:
1 | |
这里就定义了一个指向函数(这个函数参数仅仅为一个 int 类型,函数返回值是 int 类型)的指针fp。
这里东西太多,你把握不住的。
- vs2013中有这么一行代码,说明空指针的含义:
1 | |
C++11标准后,用nullptr来表示空指针。
int& r = i; 和 int r = i; 不同之处应该是内存的分配吧,后者会再开辟一个内存空间.
C++之所以增加引用类型, 主要是把它作为函数参数,以扩充函数传递数据的功能。引用和指针也很像,它们都不会创建副本,因此效率都很高。它们的主要区别在于:指针可能传递一个 NULL 过来,因此在使用前必须检查有效性;引用则必然代表某个对象,不需要做此检查。
用引用返回一个函数值的最大好处是,在内存中不产生被返回值的副本。
c++面向对象
类和对象
定义一个类,本质上是定义一个数据类型的蓝图。

类提供了对象的蓝图,所以基本上,对象是根据类来创建的。私有的成员和受保护的成员不能使用直接成员访问运算符 (.) 来直接访问。
类成员函数
类的成员函数是指那些把定义和原型写在类定义内部的函数。
成员函数可以定义在类定义内部,或者单独使用范围解析运算符 :: 来定义,在 :: 运算符之前必须使用类名。类定义中定义的成员函数把函数声明为内联的,即便没有inline关键字。内联函数指南
:: 叫作用域区分符,指明一个函数属于哪个类或一个数据属于哪个类。
:: 可以不跟类名,表示全局数据或全局函数(即非成员函数)。
- C++中函数调用非虚成员函数、调用虚函数的区别:
1.调用非虚成员函数:和调用非成员函数一样,通过对象确定对象所属的类,然后找到类的成员函数。此过程不会涉及到对象的内容,只会涉及对象的类型,是一种静态绑定。
2.调用虚函数与调用非虚成员函数不同,需通过虚函数表找到虚函数的地址,而虚函数表存放在每个对象中,不能再编译期间实现。只能在运行时绑定,是一种动态绑定。
类访问修饰符
封装是面向对象编程的重要特点,他防止函数直接访问类的内部成员。访问的权限(可见性)通过修饰符进行描述。默认是private。
| 修饰符 | 可见性 |
|---|---|
| private | 外部不可见 |
| protected | 外部不可见,子类可访问 |
| public | 外部 |
涉及到继承时,有三种继承方式,它们相应的改变了基类成员的访问属性,默认是private继承:
| 继承方式 | 基类的public成员 | 基类的protected成员 | 基类的private成员 | 继承引起的访问控制关系变化概括 |
|---|---|---|---|---|
| public继承 | 仍为public成员 | 仍为protected成员 | 不可见 | 基类的非私有成员在子类的访问属性不变 |
| protected继承 | 变为protected成员 | 变为protected成员 | 不可见 | 基类的非私有成员都为子类的保护成员 |
| private继承 | 变为private成员 | 变为private成员 | 不可见 | 基类中的非私有成员都称为子类的私有成员 |
类构造函数和析构函数
类构造函数会在每次创建对象时执行。构造函数和类名完全相同,不返回任何类型(包括void)。构造函数用于为对象成员变量初始化。默认的构造函数没有任何参数,但如果需要,可以自定义带有参数的构造函数。可以使用初始化列表来初始化字段,等效于构造函数中赋值。
类的析构函数时一类特殊成员函数,他在每次删除创建的对象时执行。析构函数和类名完全相同,只需要加一个~作为前缀,不返回值,不能带有参数。析构函数有助于在跳出程序前释放资源。
一个类可以有很多个构造函数,带参不带参都可以,析构函数就一个。
拷贝构造函数
特殊构造函数,利用同一个类之前创建的对象来初始化新创建的对象,具有单个形参,通常用const修饰,该形参是对该类类型的应用。通常用于:1、初始化新对象(显式调用)。 2、复制对象,以便将其作为参数传给函数(隐式调用)。 3、复制对象,并从函数中返回这个对象(隐式调用)。
编译器会自定义一个默认拷贝构造函数。如果类带有指针变量,并且有动态内存分配,则它必须有一个拷贝构造函数。其常见形式如下:
1 | |
关于为什么当类成员中含有指针类型成员且需要对其分配内存时,一定要有总定义拷贝构造函数??
默认的拷贝构造函数实现的只能是浅拷贝,即直接将原对象的数据成员值依次复制给新对象中对应的数据成员,并没有为新对象另外分配内存资源。
这样,如果对象的数据成员是指针,两个指针对象实际上指向的是同一块内存空间。
在某些情况下,浅拷贝回带来数据安全方面的隐患。
当类的数据成员中有指针类型时,我们就必须定义一个特定的拷贝构造函数,该拷贝构造函数不仅可以实现原对象和新对象之间数据成员的拷贝,而且可以为新的对象分配单独的内存资源,这就是深拷贝构造函数。
如何防止默认拷贝发生
声明一个私有的拷贝构造函数,这样因为拷贝构造函数是私有的,如果用户试图按值传递或函数返回该类的对象,编译器会报告错误,从而可以避免按值传递或返回对象。
总结:
当出现类的等号赋值时,会调用拷贝函数,在未定义显示拷贝构造函数的情况下,系统会调用默认的拷贝函数——即浅拷贝,它能够完成成员的一一复制。当数据成员中没有指针时,浅拷贝是可行的。但当数据成员中有指针时,如果采用简单的浅拷贝,则两类中的两个指针将指向同一个地址,当对象快结束时,会调用两次析构函数,而导致指针悬挂现象。所以,这时,必须采用深拷贝。
深拷贝与浅拷贝的区别就在于深拷贝会在堆内存中另外申请空间来储存数据,从而也就解决了指针悬挂的问题。简而言之,当数据成员中有指针时,必须要用深拷贝。
友元函数
类的友元函数是定义在类外部的,但有权访问私有成员和保护成员。尽管友元函数的原型在类的定义中出现过,但是友元函数并不是类的成员函数。友元可以是一个函数(友元函数),也可以是一个类(友元类)。友元类的所有成员都是友元。
声明一个函数是友元,需要在类定义中函数声明前加上关键字friend。
因为友元函数没有this指针,则其参数有三种情况:
- 要访问非static成员,需要对象作为参数
- 要访问static成员或者全局变量,不需要对象作为参数,可以直接访问
- 如果参数的对象是全局对象,不需要对象作为参数
内联函数
如果一个函数是内联函数,编译时,编译器会把函数的代码副本放在啊每个调用函数的地方。这就导致每次需要修改内联函数,需要重新编译所有调用内联函数的客户端。内联函数的声明方法是使用关键字inline。类定义中的函数(类成员函数)都是内联函数。
内联函数是运用了空间换时间的思想。一般来说,内联函数都比较小。
this指针
每个对象都可以通过this指针来访问自己的地址。它是所有成员函数的隐含参数。友元因为不是类的成员,因此友元没有this指针。
- 当我们调用成员函数时,实际上是替某个对象调用了它。成员函数通过一个名为this的隐式参数来访问调用它的那个对象。当我们调用一个成员函数时,用请求该函数的对象地址初始化this。例如,调用
apple.show(),编译器将apple的地址传递给show的隐式参数this,然后发生了调用。 - this是一个常量指针,不允许修改。
类的静态成员
使用static关键字把类成员定义成静态的。这意味着无论创建多少个类的对象,静态从成员都只有一个副本。并且它对于所有的对象都是共享的。如果没有初始化语句,默认会初始化为0。不能把静态成员的初始化放在类的定义中,但是可以再类的外部同各国范围解析运算符::重新声明静态变量从而对他初始化。

把函数声明为静态时,即便没有类的对象也可以调用。静态函数只需要类名加上::即可访问。静态成员函数只能访问静态成员数据,其它静态成员函数和类外部的其它函数。静态成员函数不能访问类的this指针。
静态成员变量在类中仅仅是声明,而没有定义,所以需要在类外部定义,实际上是给静态成员变量分配内存,如果不加定义会报错。初始化时赋值,而定义时分配内存,定义比初始化多了一个物理上的含义。
继承
继承是一种is a的关系,它是允许依据一种类来定义另一种类的方式。

c++支持多继承,以列表的形式排列。书写方式是:
1 | |
另外多继承(环状继承),A->D, B->D, C->(A,B),这个继承会使D创建两个对象,要解决上面问题就要用虚拟继承格式,格式:class 类名: virtual 继承方式 父类名
为什么子类的构造函数中会出现在初始化列表中呢?原因在于子类能够从基类集成的内容限制上。
我们知道,一个派生类继承了所有的基类方法,但下列情况除外:
- 基类的构造函数、析构函数和拷贝构造函数。
- 基类的重载运算符。
- 基类的友元函数。
因此,我们不能够在子类的成员函数体中调用基类的构造函数来为成员变量进行初始化。
如子类构造函数如下是错误的:
1 | |
我们可以把基类的构造函数放在子类构造函数的初始化列表上,以此实现调用基类的构造函数来为子类从基类继承的成员变量初始化。
1 | |
同时,可以验证,初始化列表在flag行之前执行。
重载运算符和重载函数
在同一作用域中队某个函数或者运算符指定多个定义,即是重载函数和重载运算符。
重载声明是指一个与之前已经在该作用域内声明过的函数或方法具有相同的名称的声明,但是它们的参数列表和定义(实现)方式不同。当调用一个重载函数或者重载运算符时,编译器会将所有参数类型与定义中的参数类型进行比较,选择最合适的定义,选择处最合适的,成为重载决策。
函数重载
在同一个作用域内,可以声明几个功能类似的同名函数,但是这些同名函数的形参必须不同。不能仅仅通过返回值类型不同重载函数。
重载运算符
大部分内置的运算符都是可以重载的。重载的运算符是带有特殊名称的函数,函数名是关键字operator后跟待重载运算符,同样的,重载运算符需要一个返回值和一个参数列表。例如:Box operator+(const Box&)这个声明是将两个Box对象相加,返回最终的Box对象。大多数的重载运算符可以被定义为非成员函数或者类成员函数。如果我们将上述的定义改成类的非成员函数,那么我们需要每次传递两个参数,如下所示Box operator+(const Box&, const Box&);
值得注意的是:
- 1、运算重载符不可以改变语法结构。
- 2、运算重载符不可以改变操作数的个数。
- 3、运算重载符不可以改变优先级。
- 4、运算重载符不可以改变结合性。
类重载、覆盖、重定义之间的区别:
重载指的是函数具有的不同的参数列表,而函数名相同的函数。重载要求参数列表必须不同,比如参数的类型不同、参数的个数不同、参数的顺序不同。如果仅仅是函数的返回值不同是没办法重载的,因为重载要求参数列表必须不同。(发生在同一个类里)
覆盖是存在类中,子类重写从基类继承过来的函数。被重写的函数不能是static的。必须是virtual的。但是函数名、返回值、参数列表都必须和基类相同(发生在基类和子类)
重定义也叫做隐藏,子类重新定义父类中有相同名称的非虚函数 ( 参数列表可以不同 ) 。(发生在基类和子类)
多态
多态按字面的意思就是多种形态。当类之间存在层次结构,并且类之间是通过继承关联时,就会用到多态。
C++ 多态意味着调用成员函数时,会根据调用函数的对象的类型来执行不同的函数。
虚函数 是在基类中使用关键字 virtual 声明的函数。在派生类中重新定义基类中定义的虚函数时,会告诉编译器不要静态链接到该函数。
我们想要的是在程序中任意点可以根据所调用的对象类型来选择调用的函数,这种操作被称为动态链接,或后期绑定。
C++中**, 虚函数**可以为private, 并且可以被子类覆盖(因为虚函数表的传递),但子类不能调用父类的private虚函数。虚函数的重载性和它声明的权限无关。
一个成员函数被定义为private属性,标志着其只能被当前类的其他成员函数(或友元函数)所访问。而virtual修饰符则强调父类的成员函数可以在子类中被重写,因为重写之时并没有与父类发生任何的调用关系,故而重写是被允许的。
编译器不检查虚函数的各类属性。被virtual修饰的成员函数,不论他们是private、protect或是public的,都会被统一的放置到虚函数表中。对父类进行派生时,子类会继承到拥有相同偏移地址的虚函数表(相同偏移地址指,各虚函数相对于VPTR指针的偏移),则子类就会被允许对这些虚函数进行重载。且重载时可以给重载函数定义新的属性,例如public,其只标志着该重载函数在该子类中的访问属性为public,和父类的private属性没有任何关系!
纯虚函数可以设计成私有的,不过这样不允许在本类之外的非友元函数中直接调用它,子类中只有覆盖这种纯虚函数的义务,却没有调用它的权利。
接口
c++接口用抽象类实现,如果类中至少有一个函数被声明为纯虚函数,则这个类就是抽象类(ABC)。纯虚函数是通过在声明中使用 “= 0” 来指定的。
1 | |
抽象类不能被用于实例化对象,它只能作为接口使用。
重写重载多态混讲
一个有趣的说法是:继承是子类使用父类的方法,而多态则是父类使用子类的方法。
重写(override):继承的时候覆盖父类的方法,重新实现一个函数体。
重载(overload):在一个类中间,同名函数参数列表不同。
多态:动态绑定。
函数的重载、运算符重载都是多态现象。 从系统实现的观点看,多态性分为两类:静态多态和动态多态性。以前学过的函数重载和运算符重载属于静态多态性,在编译程序时系统就可以确定调用哪个函数,因此静态多态性又称编译时的多态性。静态多态性是通过函数重载实现的。动态多态性是在程序运行中才能确定操作所针对的对象。它又称运行时的多态性。动态多态性是通过虚函数实现的。