- 《面向对象C++程序设计》皮德常 编著 清华大学出版社 2017
- 《C++ Primer Plus (第6版)中文版》Stephen Prata 著 张海龙 袁国忠 译 人民邮电出版社 2012
1 c 和 c++ 的区别
- C是一种结构化语言,重点在于算法和数据结构。C程序的设计首先考虑的是如何通过一个过程,对输入(或环境条件)进行运算处理得到输出(或实现过程(事务)控制)。
- 而对于C++,首先考虑的是如何构造一个对象模型,让这个模型能够契合与之对应的问题域,这样就可以通过获取对象的状态信息得到输出或实现过程(事务)控制。
- C++中new和delete是对内存分配的运算符,取代了C中的malloc和free。
- 标准C++中的字符串类取代了标准C函数库头文件中的字符数组处理函数(C中没有字符串类型)。
- C++中用来做控制态输入输出的iostream类库替代了标准C中的stdio函数库。
- C++中的try/catch/throw异常处理机制取代了标准C中的setjmp()和longjmp()函数。
- 在C中,允许有相同的函数名,不过它们的参数类型不能完全相同,这样这些函数就可以相互区别开来。而这在C语言中是不允许的。也就是C可以重载,C语言不允许。
- C语言中,允许变量定义语句在程序中的任何地方,只要在是使用它之前就可以;而C语言中,必须要在函数开头部分。而且C允许重复定义变量,C语言也是做不到这一点的
- 在C++中,除了值和指针之外,新增了引用。引用型变量是其他变量的一个别名,我们可以认为他们只是名字不相同,其他都是相同的。
- C++相对与C增加了一些关键字,如:bool、using、dynamic_cast、namespace等等
2 new/delete、malloc/free
2.1 new/delete、malloc/free 区别
特征 | new/delete | malloc/free |
---|---|---|
分配内存的位置 | 自由存储区 | 堆 |
内存分配成功的返回值 | 完整类型指针 | void* |
内存分配失败的返回值 | 默认抛出异常 | 返回 NULL |
分配内存的大小 | 由编译器根据类型计算得出 | 必须显式指定字节数 |
处理数组 | 有处理数组的 new 版本 new[] | 需要用户计算数组的大小后进行内存分配 |
是否相互调用 | 可以,看具体的 operator new/delete 实现 | 不可调用 new |
函数重载 | 允许 | 不允许 |
构造函数与析构函数 | 调用 | 不调用 |
2.2 C++有了malloc/free,为什么还需要new/delete?
- malloc/free是C++/C语言的标准库函数,new/delete是C++的运算符。他们都可用于申请动态内存和释放内存。
- 对于非内部数据类型的对象而言,只用malloc/free无法满足动态对象的要求。对象在创建的同时要自动执行构造函数,对象在消亡之前要自动执行析构函数。由于malloc/free是库函数而不是运算符,不在编译器控制权限之内,不能够把执行构造函数和析构函数的任务强加于malloc/free。
- 因此C++需要一个能完成动态内存分配和初始化工作的运算符new,以及一个能完成清理与释放内存工作的运算符delete。new/delete不是库函数,是运算符。
2.3 delete 与 delete []区别
delete 只会调用一次析构函数,而 delete[]会针对每一个成员都调用一次析构函数。
2.4 内存泄漏与定位
内存泄漏并非指的是内存在物理上的消失,而是分配某段内存后,失去了对该内存的控制,造成内存的浪费。比如 C++ new 之后没有 delete。
定位内存泄露:
- 在windows平台下通过CRT中的库函数进行检测;
- 在可能泄漏的调用前后生成块的快照,比较前后的状态,定位泄漏的位置
- Linux下通过工具valgrind检测
3 结构体、联合体
3.1 结构体与联合有何区别
- 结构体和联合都是由多个不同的数据类型成员组成,但在任何同一时刻,联合中只存放了一个被选中的成员(所有成员共用一块地址空间),而结构体的所有成员都存在(不同成员的存放地址不同)。
- 对于联合的不同成员赋值,将会对其它成员重写,原来成员的值就不存在了。而对于结构体的不同成员赋值是互不影响的。
3.2 union的好处
- 节省内存空间
- 测大小端存储(大相反(低位存高位),小相同(低位存低位),叉八六(X86计算机),必小端。)
1 | #include <stdio.h> |
解析: 10 相当于 0000 1010 低地址 1 相当于 0000 0001 高地址 如果是小端模式,低地址存放高位,高地址存放低位,那么该值按照正常顺序书写就是: 0000 0001 0000 1010,结果为266。
3.3 struct可以有构造、析构等成员函数吗?如果有,那么与class还有区别吗?
- struct可以有构造函数、析构函数,之间也可以继承。
- C++中的struct其实和class意义一样,唯一不同的就是struct里面默认的访问控制是public,class中默认的访问控制是private。
- C中存在struct关键字的唯一意义就是为了让C程序员有个归属感,是为了让C编译器兼容以前用C开发的项目。
相同点
- 两者都拥有成员函数、公有和私有部分
- 任何可以使用class完成的工作,同样可以使用struct完成
不同点
- 两者中如果不对成员不指定公私有,struct默认是公有的,class则默认是私有的
- class默认是private继承,而struct模式是public继承
- class可以作为模板类型,struct不行
引申:C++和C的struct区别
- C语言中:struct是用户自定义数据类型(UDT);C中struct是抽象数据类型(ADT),支持成员函数的定义,(C中的struct能继承,能实现多态)
- C中struct是没有权限的设置的,且struct中只能是一些变量的集合体,可以封装数据却不可以隐藏数据,而且成员不可以是函数
- C++中,struct增加了访问权限,且可以和类一样有成员函数,成员默认访问说明符为public(为了与C兼容)
- struct作为类的一种特例是用来自定义数据结构的。一个结构标记声明后,在C中必须在结构标记前加上struct,才能做结构类型名(除:typedef struct class{};);C中结构体标记(结构体名)可以直接作为结构体类型名使用,此外结构体struct在C中被当作类的一种特例
4 C语言关键字及其对应的含义
- 数据类型关键字
- A基本数据类型(5个)
- void:声明函数无返回值或无参数,声明无类型指针,显式丢弃运算结果
- char:字符型类型数据,属于整型数据的一种
- int:整型数据,通常为编译器指定的机器字长
- float:单精度浮点型数据,属于浮点数据的一种
- double:双精度浮点型数据,属于浮点数据的一种
- B类型修饰关键字(4个)
- short:修饰int,短整型数据,可省略被修饰的int。
- long:修饰int,长整形数据,可省略被修饰的int。
- signed:修饰整型数据,有符号数据类型
- unsigned:修饰整型数据,无符号数据类型
- C复杂类型关键字(5个)
- struct:结构体声明
- union:共用体声明
- enum:枚举声明
- typedef:声明类型别名
- sizeof:得到特定类型或特定类型变量的大小
- D存储级别关键字(6个)
- auto:指定为自动变量,由编译器自动分配及释放。通常在栈上分配
- static:指定为静态变量,分配在静态变量区,修饰函数时,指定函数作用域为文件内部
- register:指定为寄存器变量,建议编译器将变量存储到寄存器中使用,也可以修饰函数形参,建议编译器通过寄存器而不是堆栈传递参数
- extern:指定对应变量为外部变量,即在另外的目标文件中定义,可以认为是约定由另外文件声明的对象的一个“引用“
- const:与volatile合称“cv特性”,指定变量不可被当前线程/进程改变(但有可能被系统或其他线程/进程改变)
- volatile:与const合称“cv特性”,指定变量的值有可能会被系统或其他进程/线程改变,强制编译器每次从内存中取得该变量的值
- A基本数据类型(5个)
- 流程控制关键字
- A跳转结构(4个)
- return:用在函数体中,返回特定值(或者是void值,即不返回值)
- continue:结束当前循环,开始下一轮循环
- break:跳出当前循环或switch结构
- goto:无条件跳转语句
- B分支结构(5个)
- if:条件语句
- else:条件语句否定分支(与if连用)
- switch:开关语句(多重分支语句)
- case:开关语句中的分支标记
- default:开关语句中的“其他”分治,可选。
- C循环结构(3个):for、do、while
- A跳转结构(4个)
5 变量存储类型
5.1 C语言支持的四种变量存储类型
-
auto:auto称为自动变量(局部变量)
-
static
:static称为静态变量,根据变量的类型可以分为静态局部变量和静态全局变量。
- 静态局部变量:它与局部变量的区别在于,在函数退出时,这个变量始终存在,但不能被其它函数使用;当再次进入该函数时,将保存上次的结果。
- 静态全局变量:只在定义它的源文件中可见而在其它源文件中不可见的变量。它与全局变量的区别是:全局变量可以被其它源文件使用,而静态全局变量只能被所在的源文件使用。
-
extern:extern称为外部申明。为了使变量或者函数除了在定义它的源文件中可以使用外,还可以被其它文件使用。因此通知每一个程序模块文件,此时可用extern来说明。
-
register:register称为寄存器变量。它只能用于整型和字符型变量。定义符register说明的变量被存储在CPU的寄存器中,定义一个整型寄存器变量可写成:
register int a
;
对于以上四种数据的存储位置:register变量存在CPU的寄存器中;auto类型变量存在内存的栈;static型的局部变量和全局变量以及extern型变量(即全局变量),存在于内存的静态区。
5.2 static 的作用
-
隐藏:当同时编译多个文件时,所有未加 static 前缀的全局变量和函数都具有全局可见性,其它的源文件也能访问。如果加了 static,就会对其它源文件隐藏。利用这一特性可以在不同的文件中定义同名函数和同名变量,而不必担心命名冲突。static 可以用作函数和变量的前缀,对于函数来讲,static 的作用仅限于隐藏。
-
保持变量内容的持久:存储在静态数据区的变量会在程序刚开始运行时就完成初始化,也是唯一的一次初始化。如果作为 static 局部变量在函数内定义,它的生存期为整个源程序,但是其作用域仍与局部变量相同,只能在定义该变量的函数内使用该变量。退出该函数后,尽管该变量还继续存在,但不能使用它。
-
默认初始化为 0
-
在类中声明 static 变量或者函数时,初始化时使用作用域运算符来标明它所属类,因此,静态数据成员是类的成员,而不是对象的成员,这样就出现以下作用:
- 类的静态成员函数是属于整个类而非类的对象,所以它没有 this 指针,这就导致了它仅能访问类的静态数据和静态成员函数。
- 不能将静态成员函数定义为虚函数。
- 静态成员变量地址是指向其数据类型的指针,静态成员函数地址类型是一个“非成员函数指针”。
- static 并没有增加程序的时空开销,相反它还缩短了派生类对基类静态成员的访问时间,节省了派生类的内存空间。
- 静态数据成员是静态存储的,所以必须对它进行初始化。(程序员手动初始化,否则编译时一般不会报错,但是在链接时会报错误)
- 静态成员为基类和派生类共享,但在派生类中重复定义了基类中的静态成员,不会引起错误。
注意,静态成员初始化与一般数据成员初始化不同:
- 初始化在类体外进行,而前面不加 static,以免与一般静态变量或对象相混淆;
- 初始化时不加该成员的访问权限控制符 private,public 等;
- 初始化时使用作用域运算符来标明它所属类;
所以我们得出静态数据成员初始化的格式:
<数据类型><类名>::<静态数据成员名>=<值>
5.3 请说出 const 与#define 相比,有何优点?
const 常量有数据类型,而宏常量没有数据类型。编译器可以对前者进行类型安全检查。而对后者只进行字符替换,并且在字符替换可能会产生意料不到的错误。
const还具有以下优点:
- 提高代码安全性。
- 提高代码的可读性和可维护性。
- 提高程序的效率。
- 当 const 和 non-const 成员函数具有本质上相同的实现的时候,使用 non-const 版本调用 const 版本可以避免重复代码。
5.4 C++中顶层 const 和底层 const
如果 const 右结合修饰的为类型或者*,那这个 const 就是一个底层 const,表示指针所指向的对象是个常量。
如果 const 右结合修饰的为标识符,那这个 const 就是一个顶层 const,表示指针本身是个常量。
5.5 const 关键字的使用
- 阻止一个变量被改变,可以使用const关键字。在定义该const变量时,通常需要(必须)对它进行初始化,因为以后就没有机会再去改变它了;
- 对指针来说,可以指定指针本身为const,也可以指定指针所指的数据为const,或二者同时指定为const;
- 在一个函数声明中,const可以修饰形参,表明它是一个输入参数,在函数内部不能改变其值;
- 对于类的成员函数,若指定其为const类型,则表明其是一个常函数,不能修改类的成员变量,类的常对象只能访问类的常成员函数;
- 对于类的成员函数,有时候必须指定其返回值为const类型,以使得其返回值不为“左值”。
- const成员函数可以访问非const对象的非const数据成员、const数据成员,也可以访问const对象内的所有数据成员;
- 非const成员函数可以访问非const对象的非const数据成员、const数据成员,但不可以访问const对象的任意数据成员;
- 一个没有明确声明为const的成员函数被看作是将要修改对象中数据成员的函数,而且编译器不允许它为一个const对象所调用。因此const对象只能调用const成员函数。
- const类型变量可以通过类型转换符const_cast将const类型转换为非const类型;
- const类型变量必须定义的时候进行初始化,因此也导致如果类的成员变量有const类型的变量,那么该变量必须在类的初始化列表中进行初始化;
- 对于函数值传递的情况,因为参数传递是通过复制实参创建一个临时变量传递进函数的,函数内只能改变临时变量,但无法改变实参。则这个时候无论加不加const对实参不会产生任何影响。但是在引用或指针传递函数调用中,因为传进去的是一个引用或指针,这样函数内部可以改变引用或指针所指向的变量,这时const 才是实实在在地保护了实参所指向的变量。因为在编译阶段编译器对调用函数的选择是根据实参进行的,所以,只有引用传递和指针传递可以用是否加const来重载。一个拥有顶层const的形参无法和另一个没有顶层const的形参区分开来。
5.6 volatile 关键字的使用
- volatile 关键字是一种类型修饰符,用它声明的类型变量表示可以被某些编译器未知的因素更改,比如:操作系统、硬件或者其它线程等。遇到这个关键字声明的变量,编译器对访问该变量的代码就不再进行优化,从而可以提供对特殊地址的稳定访问。
- 当要求使用 volatile 声明的变量的值的时候,系统总是重新从它所在的内存读取数据,即使它前面的指令刚刚从该处读取过数据。
- volatile定义变量的值是易变的,每次用到这个变量的值的时候都要去重新读取这个变量的值,而不是读寄存器内的备份。多线程中被几个任务共享的变量需要定义为volatile类型。
- volatile 指针和 const 修饰词类似,const 有常量指针和指针常量的说法,volatile 也有相应的概念
- 可以把一个非volatile int赋给volatile int,但是不能把非volatile对象赋给一个volatile对象。
- 除了基本类型外,对用户定义类型也可以用volatile类型进行修饰。
- C++中一个有volatile标识符的类只能访问它接口的子集,一个由类的实现者控制的子集。用户只能用const_cast来获得对类型接口的完全访问。此外,volatile向const一样会从类传递到它的成员。
- 多线程下的volatile:有些变量是用volatile关键字声明的。当两个线程都要用到某一个变量且该变量的值会被改变时,应该用volatile声明,该关键字的作用是防止优化编译器把变量从内存装入CPU寄存器中。如果变量被装入寄存器,那么两个线程有可能一个使用内存中的变量,一个使用寄存器中的变量,这会造成程序的错误执行。volatile的意思是让编译器每次操作该变量时一定要从内存中真正取出,而不是使用已经存在寄存器中的值。
5.7 一个参数可以既是const又是volatile吗?解释为什么
- 可以。一个例子就是只读的状态寄存器。它是volatile,因为他可能被意想不到地改变;她又是const,因为程序不应该试图去改变它。
- 尽管这并不很正常。一个例子就是当一个中断服务子程序修改一个指向一个buffer的指针时。
6 C 字符串和 C++字符串的区别
- C 字符串是基本数据类型,即字符数组;C++字符串是类 string
- C 字符串函数是外部函数,字符串作为参数被传进来;C++字符串函数是字符串类内部定义的,用
.
来直接使用 - C++字符串中对一些运算符进行了重载
7 引用和指针
7.1 引用与指针区别
-
初始化区别:引用必须被初始化,指针不必。
-
可修改区别:引用初始化以后不能被改变,指针可以改变所指的对象。
-
非空区别:不存在指向空值的引用,但是存在指向空值的指针。
-
合法性区别:在使用引用之前不需要测试他的合法性;相反,指针则应该总是被测试,防止其为空。
-
应用区别
:
- 使用指针的情况
- 考虑到存在不指向任何对象的可能(在这种情况下,能够设置指针为空)
- 需要能够在不同时刻指向不同对象(在这种情况下,能够改变指针的指向)
- 使用引用的情况:总是指向一个对象并且一旦指向一个对象后就不会改变指向
- 使用指针的情况
7.2 在什么时候需要使用“常引用”?
如果既要利用引用提高程序的效率,又要保护传递给函数的数据不在函数中被改变,就应使用常引用。
7.3 将“引用”作为函数返回值类型的优点和注意事项
好处:在内存中不产生被返回值的副本,提高效率
注意事项:
- 不能返回局部变量的引用。主要原因是局部变量会在函数返回后被销毁,因此被返回的引用就成为了"无所指"的引用,程序会进入未知状态。
- 不能返回函数内部 new 分配的内存的引用。原因是引用所指向的空间就无法释放,造成内存泄漏。
- 可以返回类成员的引用,但最好是 const。主要原因是如果其它对象可以获得该属性的非常量引用(或指针),那么对该属性的单纯赋值就会破坏业务规则的完整性。
- 流操作符和赋值操作符重载返回值申明为引用。
- 在另外的一些操作符中,却千万不能返回引用,例如四则运算符。
7.4 句柄和指针的区别与联系
句柄和指针其实是两个截然不同的概念。
- Windows系统用句柄标记系统资源,隐藏系统的信息。只要知道有这个东西,然后去调用即可,他是一个32bit的uint。
- 指针则标记某个物理内存地址。
7.5 指针常量和常量指针
- 常量指针是一个指针,读成常量的指针,指向一个只读变量。如
int const *p
或const int *p
- 指针常量是一个不能给改变指向的指针。指针是个常量,不能中途改变指向,如
int *const p
技巧:* 前面的是对被指向对象的修饰,* 后面的是对指针本身的修饰。
8 递归的优缺点
- 优点:代码简洁,容易理解
- 缺点:时间效率低,递归爆栈
9 C++四种类型转换:static_cast, dynamic_cast, const_cast, reinterpret_cast
- static_cast 用的最多,能用于多态向上转化,如果向下转能成功但是不安全。
- dynamic_cast 用于动态类型转换。只能用于含有虚函数的类,用于类层次间的向上和向下转化。只能转指针或引用。向下转化时,如果是非法的对于指针返回 NULL,对于引用抛异常。
- const_cast 用于将 const 变量转为非 const
- reinterpret_cast 几乎什么都可以转,比如将 int 转指针,可能会出问题,尽量少用。
C 的强制转换表面上看起来功能强大什么都能转,但是转化不够明确,不能进行错误检查,容易出错。
10 C++是不是类型安全的?
不是。两个不同类型的指针之间可以强制转换
11 内联函数与宏定义的区别
所谓的内联函数就是那些完整地定义在类内部的函数成员。
C++ 宏定义将一个标识符定义为一个字符串,源程序中的该标识符均以指定的字符串来代替。
- 宏定义在预编译的时候就会进行宏替换;内联函数在编译阶段,在调用内联函数的地方进行替换,减少了函数的调用过程,但是使得编译文件变大。因此,内联函数适合简单函数,对于复杂函数,即使定义了内联,编译器可能也不会按照内联的方式进行编译。
- 内联函数相比宏定义更安全,内联函数可以检查参数,而宏定义只是简单的文本替换。因此推荐使用内联函数,而不是宏定义。
补充1 C笔记
- exe已停止工作:1)漏&;2)分母为0
- 定义整型,输入实型:自动截取小数部分,eg:2.5->2(赋值亦是)。
- 输出数据出错:格式字符与变量类型未对应;为赋初值。特别注意除号“/”和等号“==”。
- 数据范围与补码:-1全1,正负0全0,负数min为“1+全0”;溢出取低位,小数对应21,22,23…小数转二进制数: x/2取整数部分直到小数为0。
- else就近结合。
- || 和 && 的短路特性。
- 数组a[]: scanf("%d", a)指对a[0]赋值。
- 字符串:scanf("%s", a)->遇’ ‘, ‘\0’, ‘\t’为截点(’\n’跳出)开始不计;puts()=printf("%s", a)->遇’\0’结束;gets()遇回车结束,前无截点。
- x *= y+8 等价于 x = x + (y+8)
- C语言的函数体中,可以调用但不能定义其他函数。
- puts()自动换行,putchar()不自动换行。
补充2 C++笔记
-
endl = ‘\n’ + flush(刷新缓冲区)
-
cin跳过空白字符,cin.get()读取包含空白字符的字符
-
浮点数和整型注意转化中的除号
-
递归调用次数可用二叉树解决
-
C++, 字符串拷贝用"=", 字符串比较"=="
-
汉字占两个字符,故name[0]无法输出,"cout<<name[0]<<name[1]"输出一个汉字
-
cin对象遇’\n’结束
-
nullptr能够避免在整数(0或NULL)和指针(nullptr)之间发生混淆
-
函数传参:数据较小,按值传递,但修改时用引用;数组用指针;结构体用指针或引用;对象用引用
-
缺省函数:声明函数(最先且一次)时为其参数指定默认值
-
sizeof(): 测字节(包含’\0’)区分数组与指针strlen(): 不区分数组还是指针,读到’\0’为止返回长度(\t,\n不停)
-
全局变量: int global -> extern int global 声明到文件结尾局部变量: 从声明到函数结束静态变量: 函数退出保留值,下次调用
-
getline()读取换行符,但向数组存储时并不存储
-
初始化和赋值是不同概念
-
文本文件存储文件大小:char类型存储;二进制文件:"01"存储(内存大小)
-
引起缓存区刷新:缓存区满时(4K);程序结束;flush语句;close语句;endl语句
-
解决
:#ifdef ``` #endif 或 #pragma once -
派生类的构造函数只能描述自己的成员和基类的初始化,不能去初始化基类的成员;基类的成员需要调用基类的构造函数初始化。
-
k = (a=1, b=2, a+b)逗号运算符
-
自身类的对象不能作为类的成员(存在无限初始化问题,构造成员变量->调用自身的构造函数),而自身类对象的引用和指针可以。
-
虚函数:特殊的成员函数,用来实现运行时的多态;动态决议(运行时才绑定);基类说明virtual后,子类可缺省virtual静态成员函数:静态决议(编译时就绑定),属于整个class,并不针对某个类的实例,为所有对象所共用,作用域为全局;不能直接使用this指针;virtual不能是static
-
静态数据成员:class内声明:static int num; class外定义:int className::num(无static,默认0)
-
继承中的析构函数:基类的指针可指向派生类对象(多态性); *if delete []p;*就会调用该指针指向的派生类析构函数,而其自动调用基类的析构函数,整个派生类的对象被完全释放。若析构函数不被声明成虚函数,则编译器是是静态绑定,再删除基类指针时,只会调用基类的析构函数而不调用派生类的析构函数,这样一来派生类对象析构不完全。
-
两个指针指向同一数组,可以相减,结果是两个指针之间的元素数目。
-
函数模板:一个模板,专门用来生产函数的模板函数:是函数模板的实例化 注:类模板和模板类亦同
-
联编
:程序自身彼此关联的过程,确定程序中的操作调用与执行该操作的代码之间的关系。
- 静态联编:联编工作出现在联编阶段,用对象名或者类名来限定要调用的函数;
- 动态联编:联编工作在程序运行时执行,在程序运行时才确定将要调用的函数。
-
虚基类子对象是由最远派生类的构造函数通过调用虚基类的构造函数进行初始化的;初始化列表列出对虚基类调用,如未列出则用虚基类构造函数;先虚后非虚。
-
虚基类:多条继承路径上有一个公共的基类,公共的基类会产生多个实例。virtual class是为了实例化一次基类存在的。eg: ios类是istream和ostream类的虚基类。
-
char = int + ‘0’
-
指针数组:int *p[4]数组指针:int (*p)[4]
-
Runtime Error: 除0操作;数组越界;栈溢出;未指定值的指针进行读写操作;已经释放空间的指针再次释放。
-
数组
:
- int&float&double:初始化补零
- char: 初始化补’\0’
- 对象:调用缺省构造函数
-
常对象不能被更新,因此通过常对象只能调用他的常成员函数。
-
临时对象是在遇到其后第一个分号(语句结束处)析构的。