c++基础
C和C++的区别?
- C是面向过程的语言,C++是面向对象的语言
- C++中new和delete是对内存分配的运算符,取代了C中的malloc和free
- C++中有引用的概念,C中没有
- C++引入了类的概念,C中没有
- C++有函数重载,C中不能
- C变量只能在函数的开头处声明和定义,而C++随时定义随时使用
C++和Java之间的区别?
- Java的应用在高层,C++在中间件和底层
- Java语言简洁;取消了指针带来更高的代码质量;完全面向对象,独特的运行机制是其具有天然的可移植性。
- Java在web应用上具有C++无可比拟的优势
- 垃圾回收机制的区别。C++ 用析构函数回收垃圾,Java自动回收,写C和C++程序时一定要注意内存的申请和释放。
- Java用接口(Interface)技术取代C++程序中的多继承性
什么是面向对象?面向对象的几大特性是什么?面向对象和面向过程的区别?
- 面向对象是一种基于对象的、基于类的的软件开发思想。面向对象具有继承、封装、多态的特性。
- 面向过程:
c++特性?
1.封装:
封装的类有如下的访问类型:
数据成员通常是私有的,成员函数通常有一部分是公有的,一部分是私有的。因为类的公有的函数可以在类外被访问,也称之为类的接口。(实际中具体的访问权限情况根据实际情况而定)
封装的优点: 程序更模块化,更易读易写,提升了代码重用到一个更高的层次。其实我认为更重要的是保密性和跨平台性。
[x]封装成dll动态链接库后也可以实现程序的保密性(需要提供项目代码时,部分核心算法可以封装起来!)
[x]同时一个Vber使用他的VB程序调用你的c++算法时,通过c++封装成dll可以实现跨平台性的使用。(反过来调用比较麻烦,参考COM组件的使用)
值得注意的是类的封装要满足单一职责原则,只完成单独的使命,万万不可涂一时之快,写成了武林全集的dll,后期维护就很头疼了。
2.继承:
3.多态:
指针和引用的区别
- 指针保存的是指向对象的地址,引用相当于变量的别名
- 引用在定义的时候必须初始化,指针没有这个要求
- 指针可以改变地址,引用必须从一而终
- 不存在空应引用,但是存在空指针NULL,相对而言引用更加安全
- 引用的创建不会调用类的拷贝构造函数
c/c++中的内存分区
- 栈区:由编译器自动分配和释放,用来存放函数的参数、局部变量。存放在栈中的数据只在当前函数以及下一层函数中有效,函数一旦结束,这些数据就被释放了;
- 堆区:有程序员手动分配和释放,如果程序员没有释放则在整个程序结束的时候用OS释放,忘记释放的话就会出现内存泄漏的问题;
- 全局(静态)区:用来存储全局变量和静态变量,程序结束后由OS负责释放;
- 常量区:存放字面量,不允许修改,如“hello world”,2 , ‘c’,程序结束的时候由OS负责释放;
- 代码区:存放代码(如函数),不允许修改,但可以执行;
c/c++中的内存分配
- 在静态存储区分配:内存在程序编译的时候就已经分配好了,这块内存在程序运行的整个期间都存在。例如:全局变量、static变量;
- 在栈上分配:函数内申请的局部变量,在函数执行结束后就会被释放掉,效率高,但是栈的内存容量有限;
- 从堆上分配:也称之为动态内存分配,程序在运行期间使用malloc和new申请任意的多少空间,程序员自己负责在何时使用free或delete释放掉申请的那块内存。这部分内存的生命周期是人为决定的,使用上非常灵活,但是有人为介入,所以忘记释放掉这块内存的时候就会出现内存泄露的问题,并且频繁的分配和释放不同大小的堆内存会产生内存碎片的问题;
new/delete与malloc/free的区别
- new是运算符,malloc是C语言库函数
- new可以重载,malloc不能重载
- new的变量是数据类型,malloc的是字节大小
- new可以调用构造函数,delete可以调用析构函数,malloc/free不能
- new返回的是指定对象的指针,而malloc返回的是void*,因此malloc的返回值一般都需要进行类型转化
- malloc分配的内存不够的时候可以使用realloc扩容,new没有这样的操作
- new内存分配失败抛出bad_malloc,malloc内存分配失败返回NULL值
- 申请内存的位置不同;new操作符是从自由存储区上为对象动态分配内存空间的,而malloc函数则是从堆上分配内存空间的;(自由存储区是c++基于new操作符的一个抽象概念,凡是通过new操作符进行内存申请的,该内存就被称之为自由存储区。该存储区的位置是由new在哪里为对象分配内存决定的,既可以是堆,也可以是静态存储区)
volatile关键字
- C/C++ 中的 volatile 关键字和 const 对应,用来修饰变量,通常用于建立语言级别的 memory barrier(内存屏障)。
- volatile 关键字是一种类型修饰符,用它声明的类型变量表示可以被某些编译器未知的因素更改,比如:操作系统、硬件或者其它线程等。遇到这个关键字声明的变量,编译器对访问该变量的代码就不再进行优化,从而可以提供对特殊地址的稳定访问。声明时语法:int volatile vInt; 当要求使用 volatile 声明的变量的值的时候,系统总是重新从它所在的内存读取数据,即使它前面的指令刚刚从该处读取过数据,而且读取的数据立刻被保存,这样可以防止出现由于编译器优化读取和存储,可能暂时使用寄存器中的值,如果这个变量由别的程序更新的话,将出现变量不一致的情况。
- 访问寄存器要比访问内存要块,因此CPU会优先访问该数据在寄存器中的存储结果,但是内存中的数据可能已经发生了改变,而寄存器中还保留着原来的结果。为了避免这种情况的发生将该变量声明为volatile,告诉CPU每次都从内存去读取数据。
- 一个参数可以即是const又是volatile的吗?可以,一个例子是只读状态寄存器,是volatile是因为要告诉编译器它可能被其他程序改变,而是const告诉在程序里面不应该试图去修改它,但是它是可以被程序外的东西修改。
- 举例说明:在DSP开发(数字信号处理开发)中,经常要等待某个时间的触发,所以经常会写出这样的程序:
short flag; void test() { do1(); while(flag == 0); do2(); } //这段程序等待内存变量flag的值变为非0之后再运行do2(),变量flag的值由别的程序更改, //这个程序可能是某个硬件中断服务程序。例如:如果某个按钮按下的话,就会对DSP产生中断, //在按键中断程序中修改flag为1,这样上面的程序就能够得以继续运行。但是, //编译器并不知道flag的值会被别的程序修改,因此在它进行优化的时候,可能会把flag的值先 //读入某个寄存器,然后等待那个寄存器变为1。如果不幸进行了这样的优化, //那么while循环就变成了死循环,因为寄存器的内容不可能被中断服务程序修改。 //为了让程序每次都读取真正flag变量的值,就需要定义为如下形式: volatile short flag;
需要注意的是,没有volatile也可能能正常运行,但是可能修改了编译器的优化级别之后就又不能正常运行了。因此经常会出现debug版本正常,但是release版本却不能正常的问题。所以为了安全起见,只要是等待别的程序修改某个变量的话,就加上volatile关键字。volatile的本意是“易变的”,由于访问寄存器的速度要快过RAM,所以编译器一般都会作减少存取外部RAM的优化,所以为了不让编译器进行这种优化,就需要加volatile。
6.使用场景static关键字的作用
- 修饰全局变量
- 修饰局部变量
- 修饰全局函数 --- 限制它的作用域只能在本文件之内。可以和其他文件的函数重名
- 修饰局部函数
- 修饰类的成员变量、成员函数
静态全局变量、全局变量、静态局部变量、局部变量的区别
- 静态全局变量和全局变量都属于全局区(静态区),区别在于静态全局变量只作用于本文件中,而全局变量可以被其他文件调用;
- 静态局部变量和局部变量的区别:静态局部变量属于全局区,而函数内部的局部变量属于栈区;静态局部变量在函数执行结束后,不会被销毁,且不能被其他函数调用,会在整个程序结束后被OS回收。而局部变量在函数调用结束后就会被销毁;静态局部变量子啊编译期间只会赋值一次,以后每次函数被调用的时候,都不会再进行赋值操作了。而局部变量,函数调用一次,就赋值一次;静态局部变量如果在定义的时候没有赋初值,就会默认为0,局部变量则为一个随机值。
- 一个参数可以即是const又是volatile的吗?可以,一个例子是只读状态寄存器,是volatile是因为它可能被意想不到的被改变,是const告诉程序不应该试图去修改他
extern关键字作用
声明一个外部变量。
const关键字的作用
- const修饰全局变量
- const修饰局部变量
- const *常量指针,内容不可以改
- * const 指针常量 ,地址不可以改
- *const* 指向常量的指针常量,内容地址都不可以改
- const修饰引用做形参
- const修饰成员变量,必须在构造函数列表中初始化
- const修饰成员函数,不能和static关键字同时使用,因为static关键字修饰静态成员函数,静态成员函数中不包含this指针,即不能被实例化,而const修饰的实际是成员函数的this指针。const成员函数只能调用const成员函数,const类对象只能调用const成员函数
- const修饰类对象:对象的任何成员都不可以被修改,只能调用const成员函数
- 构造函数和静态成员函数不可以声明为const函数
define/const/inline的区别
本质:define只是字符串替换,const参与编译运行,具体的:
- define不会做类型检查,const拥有类型,会执行相应的类型检查
- define仅仅是宏替换,不占用内存,而const会占用内存
- const内存效率更高,编译器通常将const变量保存在符号表中,而不会分配存储空间,这使得它成为一个编译期间的常量,没有存储和读取的操作
- 内联函数通过在函数定义前加inline关键字实现,他既有函数的优点,又有带参宏的优点。
- 内联函数本质上是函数,所以有函数的优点(内联函数是编译器负责处理的,编译器可以帮我们做参数的静态类型检查);但是他同时也有带参宏的优点(不用调用开销,而是原地展开)。所以几乎可以这样认为:内联函数就是带了参数静态类型检查的宏。
- 当我们的函数内函数体很短(譬如只有一两句话)的时候,我们又希望利用编译器的参数类型检查来排错,我还希望没有调用开销时,最适合使用内联函数。
#include <iostream> using namespace std; int main () { cout << "Value of __LINE__ : " << __LINE__ << endl; cout << "Value of __FILE__ : " << __FILE__ << endl; cout << "Value of __DATE__ : " << __DATE__ << endl; cout << "Value of __TIME__ : " << __TIME__ << endl; return 0; }
Value of __LINE__ : 6 Value of __FILE__ : test.cpp Value of __DATE__ : Feb 28 2011 Value of __TIME__ : 18:52:48
c语言的预处理理论
1.从源码到可执行文件的过程:
- 源码.c->(编译)->elf可执行程序
- 源码.c->(编译)->目标文件.o->(链接)->elf可执行程序
- 源码.c->(编译)->汇编文件.S->(汇编)->目标文件.o->(链接)->elf可执行程序
- 源码.c->(预处理)->预处理过的.i源文件->(编译)->汇编文件.S->(汇编)->目标文件.o->(链接)->elf可执行程序
2.预处理的意义
- 编译器本身的主要目的是编译源代码,将C的源代码转化成.S的汇编代码。编译器聚焦核心功能后,就剥离出了一些非核心的功能到预处理器去了。
- 预处理器帮编译器做一些编译前的杂事。
3.编程中常见的预处理
- #include(#include <>和#include ""的区别):#include< > 引用的是编译器的类库路径里面的头文件(就是系统自带的,不是程序员自己写的); #include" " 引用的是你程序目录的相对路径中的头文件。更深层次来说:<>的话C语言编译器只会到系统指定目录(编译器中配置的或者操作系统配置的寻找目录,譬如在ubuntu中是/usr/include目录,编译器还允许用-I来附加指定其他的包含路径)去寻找这个头文件(隐含意思就是不会找当前目录下),如果找不到就会提示这个头文件不存在。
""包含的头文件,编译器默认会先在当前目录下寻找相应的头文件,如果没找到然后再到系统指定目录去寻找,如果还没找到则提示文件不存在。
- 注释;
编译器既然不看注释,那么编译时最好没有注释的。实际上在预处理阶段,预处理器会拿掉程序中所有的注释语句,到了编译器编译阶段程序中其实已经没有注释了。
- #if #elif #endif #ifdef
- 宏定义;
4.gcc中只预处理不编译的方法
- gcc编译时可以给一些参数来做一些设置,譬如gcc xx.c -o xx可以指定可执行程序的名称;譬如gcc xx.c -c -o xx.o可以指定只编译不链接,也可以生成.o的目标文件。
- gcc -E xx.c -o xx.i可以实现只预处理不编译。一般情况下没必要只预处理不编译,但有时候这种技巧可以用来帮助我们研究预处理过程,帮助debug程序。
- 总结:宏定义被预处理时的现象有:第一,宏定义语句本身不见了(可见编译器根本就不认识#define,编译器根本不知道还有个宏定义);第二,typedef重命名语言还在,说明它和宏定义是有本质区别的(说明typedef是由编译器来处理而不是预处理器处理的);
#if...#endif条件编译预处理命令
1.条件编译相关的预编译指令:
#define 定义一个预处理宏 #undef 取消宏的定义 #if 编译预处理中的条件命令,相当于C语法中的if语句 #ifdef 判断某个宏是否被定义,若已定义,执行随后的语句 #ifndef 与#ifdef相反,判断某个宏是否未被定义 #elif 若#if, #ifdef, #ifndef或前面的#elif条件不满足,则执行#elif之后的语句,相当于C语法中的else-if #else 与#if, #ifdef, #ifndef对应, 若这些条件不满足,则执行#else之后的语句,相当于C语法中的else #endif #if, #ifdef, #ifndef这些条件命令的结束标志. defined 与#if, #elif配合使用,判断某个宏是否被定义
2.两种格式
#ifdef 标示符 程序段1 #else 程序段2 #endif
(2)格式2
#if 表达式 程序段1 #else 程序段2 #endif
3.应用举例
最常见的条件编译是防止重复包含头文件的宏,形式跟下面代码类似:
#ifndef ABCD_H #define ABCD_H // ... some declaration codes #endif // #ifndef ABCD_H(2)在实现文件中通常有如下类似的定义:
#ifdef _DEBUG // ... do some operations #endif #ifdef _WIN32 // ... use Win32 API #endif(3)defined
defined用来测试某个宏是否被定义。defined(name): 若宏被定义,则返回1,否则返回0。它与#if、#elif、#else结合使用来判断宏是否被定义,乍一看好像它显得多余, 因为已经有了#ifdef和#ifndef。defined可用于在一条判断语句中声明多个判别条件;#ifdef和#ifndef则仅支持判断一个宏是否定义。
#if defined(VAX) && defined(UNIX) && !defined(DEBUG)
#ifdef ABC // ... codes while definded ABC #elif (CODE_VERSION > 2) // ... codes while CODE_VERSION > 2 #else // ... remained cases #endif // #ifdef ABC#ifdef用于判断某个宏是否定义,和#ifndef功能正好相反,二者仅支持判断单个宏是否已经定义,上面例子中二者可以互换。如果不需要多条件预编译的话,上面例子中的#elif和#else均可以不写。
#if 常量表达式1 // ... some codes #elif 常量表达式2 // ... other codes #elif 常量表达式3 // ... ... #else // ... statement #endif
常量表达式可以是包含宏、算术运算、逻辑运算等等的合法C常量表达式,如果常量表达式为一个未定义的宏, 那么它的值被视为0。
#if MACRO_NON_DEFINED // 等价于 #if 0
在判断某个宏是否被定义时,应当避免使用#if,因为该宏的值可能就是被定义为0。而应当使用#ifdef或#ifndef。
注意: #if、#elif之后的宏只能是对象宏。如果宏未定义,或者该宏是函数宏,则编译器可能会有对应宏未定义的警告。
4.意义和妙用
#include <stdio.h> #include "b.h"
b.h
#include "a.h"
c.c
#include "a.h" #include "b.h" int main() { printf("Hello!"); return 0; }
如果你程序是这样写的话,编译器就会出现Error #include nested too deeply的错误。
因为这里 b.h 和 a.h 都互相包含,c.c文件在include的时候重复include了a.h,我们希望c.c文件中执行#include "b.h"的时候 b.h 能进行判断,如果没有#include "a.h"则include,如果已经include了,则不再重复定义。
可以将b.h修改为:
#ifndef _A_H #define _A_H #include "a.h" #endif头文件的中的#ifndef,这是一个很关键的东西。比如你有两个C文件,这两个C文件都include了同一个头文件。而编译时,这两个C文件要一同编译成一个可运行文件,于是问题来了,大量的声明冲突。
还是把头文件的内容都放在#ifndef和#endif中吧。不管你的头文件会不会被多个文件引用,你都要加上这个。一般格式是这样的:
#ifndef <标识> #define <标识> ...... ...... #endif
<标识>在理论上来说可以是自由命名的,但每个头文件的这个“标识”都应该是唯一的。标识的命名规则一般是头文件名全大写,前后加下划线,并把文件名中的“.”也变成下划线,如:stdio.h
#ifndef _STDIO_H_ #define _STDIO_H_ ...... #endif
在程序首部定义#ifdef HNLD: #ifdef HNLD include"n166_hn.c" #endif如果不许向别的用户提供该功能,则在编译之前将首部的HNLD加一下划线即可。
【2】在每一个子程序前加上标记,以便追踪程序的运行。
#ifdef DEBUG printf(" Now is in hunan !"); #endif
【3】避开硬件的限制。有时一些具体应用环境的硬件不一样,但限于条件,本地缺乏这种设备,于是绕过硬件,直接写出预期结果。具体做法是:
#ifndef TEST i=dial(); //程序调试运行时绕过此语句 #else i=0; #endif调试通过后,再屏蔽TEST的定义并重新编译,即可发给用户使用了。
在#ifndef中定义变量出现的问题(一般不定义在#ifndef中)。
#ifndef AAA #define AAA ... int i; ... #endif里面有一个变量定义,在vc中链接时就出现了i重复定义的错误,而在c中成功编译。
结论:
【1】当你第一个使用这个头的.cpp文件生成.obj的时候,int i 在里面定义了当另外一个使用这个的.cpp再次[单独]生成.obj的时候,int i 又被定义然后两个obj被另外一个.cpp也include 这个头的,连接在一起,就会出现重复定义.
【2】把源程序文件扩展名改成.c后,VC按照C语言的语法对源程序进行编译,而不是C++。在C语言中,若是遇到多个int i,则自动认为其中一个是定义,其他的是声明。
【3】C语言和C++语言连接结果不同,可能(猜测)是在进行编译的时候,C++语言将全局
变量默认为强符号,所以连接出错。C语言则依照是否初始化进行强弱的判断的。(参考)
解决方法:
【1】把源程序文件扩展名改成.c。
【2】推荐解决方案:
<x.h> #ifndef __X_H__ #define __X_H__ extern int i; #endif //__X_H__ <x.c> int i;注意问题:变量一般不要定义在.h文件中。
宏函数的使用和注意事项
1.定义
2.注意事项:
#define Max(a,b) (((a)>(b)) ? (a) : (b))
关键:
第一点:要想到使用三目运算符来完成。
第二点:注意括号的使用
#define MAX(x, y) ({\ int _x = x;\ int _y = y;\ _x > _y ? _x : _y;\ }) //适用各种类型的,类型函数模板 #define MAX(x, y) ({\ typeof(x) _x = x;\ typeof(y) _y = y;\ _x > _y ? _x : _y;\ }) //x,y类型不同的时候提示不能进行比较 #define MAX(x, y) ({\ typeof(x) _x = x;\ typeof(y) _y = y;\ _x > _y ? _x : _y;\ })宏定义示例2:SEC_PER_YEAR,用宏定义表示一年中有多少秒
#define year (365UL*24*60*60)
关键:
第一点:当一个数字直接出现在程序中时,它的是类型默认是int
第二点:一年有多少秒,这个数字不知道是否超过了int类型存储的范围
#include <stdio.h> #define M 10 #define N M #define X(a,b) ((a)*(b)) #define Max(a,b) (((a)>(b)) ? (a) : (b)) //#define year (365*24*60*60)UL //在Linux中GCC测试出错 #define year (365UL *24*60*60) //在Linux中GCC测试正确 int main(void) { int i = N; int k, x, y, m, n; printf("%d\n",i); //输出:10 k = X(3+i, i - 8); //解析为:k = ((3+i)*(i - 8)); printf("k = %d\n",k); //输出:26 m = Max(3,5); //解析为:m = (((3)>(5)) ? (3) : (5)); printf("Max is %d\n",m);//输出:5 n = year; //解析为:n = (365UL*24*60*60); printf("%d\n", n); //输出:31536000 return 0 ; }
3.带参宏和带参函数的区别(宏定义的缺陷)
- 宏定义是在预处理期间处理的,而函数是在编译期间处理的。这个区别带来的实质差异是:宏定义最终是在调用宏的地方把宏体原地展开,而函数是在调用函数处跳转到函数中去执行,执行完后再跳转回来。
- 注:宏定义和函数的最大差别就是:宏定义是原地展开,因此没有调用开销;而函数是跳转执行再返回,因此函数有比较大的调用开销。所以宏定义和函数相比,优势就是没有调用开销,没有传参开销,所以当函数体很短(尤其是只有一句话时)可以用宏定义来替代,这样效率高。
- 带参宏和带参函数的一个重要差别就是:宏定义不会检查参数的类型,返回值也不会附带类型;而函数有明确的参数类型和返回值类型。当我们调用函数时编译器会帮我们做参数的静态类型检查,如果编译器发现我们实际传参和参数声明不同时会报警告或错误。
- 注:用函数的时候程序员不太用操心类型不匹配因为编译器会检查,如果不匹配编译器会报警;用宏的时候程序员必须很注意实际传参和宏所希望的参数类型一致,否则可能编译不报错但是运行有误。
- 总结:宏和函数各有千秋,各有优劣。总的来说,如果代码比较多用函数适合而且不影响效率;但是对于那些只有一两句话的函数开销就太大了,适合用带参宏。但是用带参宏又有缺点:不检查参数类型。
函数的本质
1.C语言为什么会有函数
- 整个程序分成多个源文件,一个文件分成多个函数,一个函数分成多个语句,这就是整个程序的组织形式。这样组织的好处在于:分化问题、便于编写程序、便于分工。
- 函数的出现是人(程序员和架构师)的需要,而不是机器(编译器、CPU)的需要。
- 函数的目的就是实现模块化编程。说白了就是为了提供程序的可移植性。
2.函数书写的一般原则:
- 遵循一定的格式。函数的返回类型、函数名、参数列表等。
- 一个函数只做一件事。函数不能太长也不宜太短,原则上一个函数只做一件事情。
- 传参不宜过多。在ARM体系下,传参不宜超过4个,如果传参需要多个返回值,则考虑结构体打包。
- 尽量少碰全局变量。函数最好用传参返回值和外部交换数据,不要用全局变量(不利于函数移植);
3.函数是动词、变量是名词(面向对象中分别称之为方法和成员变量)
- 函数将来被编译成可执行代码段,变量(主要指全局变量)经过编译后变成数据或者在运行时变成数据。一个程序的运行需要代码和数据两方向的结合才能完成。
- 代码和数据需要彼此配合,代码是为了加工数据,数据必须借助代码来起作用。拿现实中的工厂来比喻:数据是原材料,代码是加工流水线。名词性的数据必须经过动词性的加工才能变成最终我们需要的产出的数据。这个加工的过程就是程序的执行过程。
4.函数的实质 --- 数据处理器
- 程序的主体是数据,也就是说程序运行的主要目标是生成目标数据,我们写代码也是为了目标数据。我们如何得到目标数据?必须2个因素:原材料+加工算法。原材料就是程序的输入数据,加工算法就是程序。
- 程序的编写和运行就是为了把原数据加工成目标数据,所以程序的实质就是一个数据处理器。
- 函数就是程序的一个缩影,函数的参数列表其实就是为了给函数输入原材料数据,函数的返回值和输出型参数就是为了向外部输出目标数据,函数的函数体里的那些代码就是加工算法。
- 函数在静止没有执行(乖乖的躺在硬盘里)的时候就好象一台没有开动的机器,此时只占一些存储空间但是并不占用资源(CPU+内存);函数的每一次运行就好象机器的每一次开机运行,运行时需要耗费资源(CPU+内存),运行时可以对数据加工生成目标数据;函数运行完毕会释放占用的资源。
- 整个程序的运行其实就是很多个函数相继运行的连续过程。
5.函数的基本使用
(1)函数三要素:定义、声明、调用
- 函数的定义就是函数体、函数声明是函数原型、函数调用就是使用函数
- 函数定义是函数的根本,函数定义中的函数名表示了这个函数在内存中的首地址,所以可以用函数名来调用执行这个函数(实质是指针解引用访问);函数定义中的函数体是函数的执行关键,函数将来执行时主要就是执行函数体。所以一个函数没有定义就是无基之塔。
- 函数声明的主要作用是告诉编译器函数的原型
- 函数调用就是调用执行一个函数。
(2)函数的原型和作用
- 函数原型就是函数的声明,说白了就是函数的函数名、返回值类型、参数列表。
- 函数原型的主要作用就是给编译器提供原型,让编译器在编译程序时帮我们进行参数的静态类型检查
- 必须明白:编译器在编译程序时是以单个源文件为单位的(所以一定要在哪里调用在哪里声明),而且编译器工作时已经经过预处理处理了,最最重要的是编译器编译文件时是按照文件中语句的先后顺序执行的。
- 编译器从源文件的第一行开始编译,遇到函数声明时就会收到编译器的函数声明表中,然后继续向后。当遇到一个函数调用时,就在我的本文件的函数声明表中去查这个函数,看有没有原型相对应的一个函数(这个相对应的函数有且只能有一个)。如果没有或者只有部分匹配则会报错或报警告;如果发现多个则会报错或报警告(函数重复了,C语言中不允许2个函数原型完全一样,这个过程其实是在编译器遇到函数定义时完成的。所以函数可以重复声明但是不能重复定义)
6.递归函数
(1)什么是递归函数
- 递归函数就是函数中调用了自己本身这个函数的函数。
- 递归函数和循环的区别。递归不等于循环
- 递归函数解决问题的典型就是:求阶乘、求斐波那契数列
(2)递归调用的原理
- 实际上递归函数是在栈内存上递归执行的,每次递归执行一次就需要耗费一些栈内存。
- 栈内存的大小是限制递归深度的重要因素。
(3)使用递归函数的原则:收敛性、栈溢出
- 收敛性就是说:递归函数必须有一个终止递归的条件。当每次这个函数被执行时,我们判断一个条件决定是否继续递归,这个条件最终必须能够被满足。如果没有递归终止条件或者这个条件永远不能被满足,则这个递归没有收敛性,这个递归最终要失败。
- 因为递归是占用栈内存的,每次递归调用都会消耗一些栈内存。因此必须在栈内存耗尽之前递归收敛(终止),否则就会栈溢出。
- 递归函数的使用是有一定风险的,必须把握好。
C++构造函数能抛异常吗?析构呢?
-
构造函数和析构函数中的异常
1、构造函数可以抛出异常。
2、c++标准指明析构函数不能、也不应该抛出异常。
more effective c++关于第2点提出两点理由:
1)如果析构函数抛出异常,则异常点之后的程序不会执行,如果析构函数在异常点之后执行了某些必要的动作比如释放某些资源,则这些动作不会执行,会造成诸如资源泄漏的问题。
2)通常异常发生时,c++的机制会调用已经构造对象的析构函数来释放资源,此时若析构函数本身也抛出异常,则前一个异常尚未处理,又有新的异常,会造成程序崩溃的问题。
解决办法:
1)永远不要在析构函数抛出异常。
2)通常第一点有时候不能保证。可以采取如下的方法~ClassName() { try{ do_something(); } catch( ){ // 这里可以什么都不做,只是保证catch块的程序抛出的异常不会被扔出析构函数之外。 } }
- 虚函数的执行依赖于虚函数表(对象内存的首地址存放的是虚函数列表的首地址)。而虚函数表需要在构造函数中进行初始化工作,即初始化vptr,让他指向正确的虚函数表。而在构造对象期间,虚函数表还没有被初始化,将无法进行。
- 在类的继承中,如果有基类指针指向派生类,那么用基类指针delete时,如果不定义成虚函数,派生类中派生的那部分无法析构。通常应该为基类提供一个虚析构函数,这样派生类对象在释放度的时候,会先调用派生类析构,再调用基类析构,避免出现内存泄露的问题。
- 构造函数不要调用虚函数。在基类构造的时候,虚函数是非虚,不会走到派生类中,既是采用的静态绑定。显然的是:当我们构造一个子类的对象时,先调用基类的构造函数,构造子类中基类部分,子类还没有构造,还没有初始化,如果在基类的构造中调用虚函数,如果可以的话就是调用一个还没有被初始化的对象,那是很危险的,所以C++中是不可以在构造父类对象部分的时候调用子类的虚函数实现。但是不是说你不可以那么写程序,你这么写,编译器也不会报错。只是你如果这么写的话编译器不会给你调用子类的实现,而是还是调用基类的实现。
有哪些内存泄漏?如何判断内存泄漏?如何定位内存泄漏?野指针问题?
==内存泄漏类型:==
- 堆内存泄漏 (Heap leak)。堆内存指的是程序运行中根据需要分配通过malloc,realloc new等从堆中分配的一块内存,再是完成后必须通过调用对应的 free或者delete 删掉。如果程序的设计的错误导致这部分内存没有被释放,那么此后这块内存将不会被使用,就会产生Heap Leak.
- 系统资源泄露(Resource Leak).主要指程序使用系统分配的资源比如 Bitmap,handle ,SOCKET等没有使用相应的函数释放掉,导致系统资源的浪费,严重可导致系统效能降低,系统运行不稳定。
==检测内存泄漏:==
- 在windows平台下通过CRT中的库函数进行检测;
- 在可能泄漏的调用前后生成块的快照,比较前后的状态,定位泄漏的位置
- Linux下通过工具valgrind检测
==野指针:指向被释放的或者访问受限的内存的指针==
- 指针变量没有被初始化(如果值不定,可以初始化为NULL);
- 指针被free或delete以后,没有置为NULL,free和delete只是把指针指向的内存给释放掉了,并没有把指针本身释放掉,此时的指针直系那个的是“垃圾”内存,应该将释放后的指针置为NULL
- 指针操作超越了变量的作用范围,比如返回指向栈内存的指针就是野指针;
c++智能指针
- c++里面有四个智能指针:auto_ptr , unique_ptr , shared_ptr , weak_ptr,其中后三个是c++11支持的,并且第一个已经被c++11弃用了;
- 作用:用来管理堆上分配的内存,它将普通的指针封装成一个栈对象,当栈对象的生存周期结束后,会在析构函数中释放掉申请的内存,从而防止内存泄露。
c++动态内存
- new/delete 、 new 变量类型[]/delete[] 数组名 、 malloc/free;前面有
- 分配异常机制:
foo = new int[5];//如果分配失败的话,就会抛出异常(2)nothrow:当内存分配失败的时候,不抛出bad_alloc异常或终止程序,而是返回一个空指针,程序继续执行下去;
int * foo = new (nothrow)int [5]; if(foo == nullptr) { dosomething(); }
c++11新特性
- auto关键字
auto关键字可以帮助我们分析表达式所属的类型,让编译器自动分析某个初始值来判断它所属的类型。当然,使用auto关键字必须确定初始值。 int main() { auto a = make_shared<string>("hello world"); } //我们定义了一个变量a,它的初始值是一个make_share类型。这样我们就新建了一个指向string //智能指针shared_ptr;相比于之前的c++标准,这样会更加简便,利于我们快速完成程序设计;
2.decltype关键字
int main() { const int b = 10; int c = 20; decltype(b) x = 0; decltype(c) y = 1; cout << x << " " << y << endl; } //定义一个常量b,设定其初始值为10,又定义了一个初值为20的变量c,然后通过decltype关键字定义 //了两个变量x、y。让编译器通过推断括号里的表达式来判断x,y的类型。
3.字面值nullptr
int main() { int *p = nullptr; int *q = new int(10); p = q; delete q; }
4.constexpr关键字
int main() { constexpr const int val = 100; constexpr int val2 = val + 1; } //我们定义了一个常量val为100,并用其来初始化另外一个常量val2
5.范围for语句(区间迭代)
这种遍历语句指定序列的每个元素,并且可以对每个元素进行操作int main() { std::vector<int> arr(5 , 100); for(std::vector<int>::iterator i = arr.begin();i != arr.end(); ++i) { std::cout << *i << std::endl; } //在引入了范围for语句后: for(auto i : arr) std::cout << i << std::endl; }
6.Lambda表达式
int main() { int a = 0; auto f = [=]{return a;} a += 1; cout << f() << endl;//输出0 int b = 0; auto f1 = [&a]{return a;} b += 1; cout << f1() << endl;//输出1 } //[capture list] (parameter list) ->return type{function body} //capture list捕获列表,也就是lambda所在函数中的局部变量的列表,如果没有,可为空; // 用来控制lambda表达式所能够访问的外部变量,以及如何去访问这些变量; // (1)[]不捕获任何变量 // (2)[&]捕获外部作用域中所有变量,并作为函数引用在函数体中使用(即按引用捕获) // (3)[=]捕获外部作用域中所有变量,并作为副本在函数体中使用(即按值捕获) // (4)[= , &foo]按值捕获外部作用域中所有的变量,并按引用捕获foo变量 // (5)[bar]按值捕获bar变量,同时不捕获其他变量 // (6)[this]捕获当前类中的this指针,让lambda表达式拥有和当前类成员函数通样的访问权限 //return type表示该lambda表达式的返回类型; //parameter list为形参列表 //注意:如果有返回类型,lambda表达式必须使用尾置返回来确定类型。 //lambda表达式必须包括捕获列表和函数体,另外几个可以省略;
7.初始化列表
#include <initializer_list> struct A{ int a; float b; }; struct B{ B(int _a , float _b):a(_a) , b(_b){} private: int a; float b; }; class Magic{ public: Magic(std::initializer_list<int> list){} } int main() { //统一的初始化语法 A a{1 , 1.1}; B b{2, 2.2}; Magic magic = {1,2,3,4,5}; std::vector<int> vec = {1,2,3,4,5}; }
8.模板增强
template class std::vector<bool>;//强行实例化 extern template class std::vector<double>;//不在该编译文件中实例化模板(2)尖括号>
std::vector<std::vector<int> > vec; //使用模板类vector定义了一个二维向量容器 std::vector<std::vector<int>> vec;//连续的>>变得合法了,可以顺利通过编译(3)类型别名模板
template <typename T> using newtype = sunType<int , T , 1>;(4)默认模板参数
//我们可能定义了一个加法函数: template<typename T, typename U> auto add(T x, U y) -> decltype(x+y) { return x+y } //但在使用时发现,要使用 add,就必须每次都指定其模板参数的类型。 //在 C++11 中提供了一种便利,可以指定模板的默认参数: template<typename T = int, typename U = int> auto add(T x, U y) -> decltype(x+y) { return x+y; }
9.构造函数
class Base{ pubulic: int val1; int val2; Base() { val1 = 1; } Base(int value):Base()//委托构造,将val的值赋值为1(不管传的参数value是多少),val2的值赋值为2; { val2 = 2; } }(2)继承构造
struct A { A(int i) {} A(double d,int i){} A(float f,int i,const char* c){} //...等等系列的构造函数版本 }; struct B:A { B(int i):A(i){} B(double d,int i):A(d,i){} B(folat f,int i,const char* c):A(f,i,e){} //......等等好多个和基类构造函数对应的构造函数 }; //在c++11中的继承构造: struct A { A(int i) {} A(double d,int i){} A(float f,int i,const char* c){} //...等等系列的构造函数版本 }; struct B:A { using A::A; //关于基类各构造函数的继承一句话搞定 //...... };
10。新增容器
std::array<int, 4> arr= {1,2,3,4}; int len = 4; std::array<int, len> arr = {1,2,3,4}; // 非法, 数组大小参数必须是常量表达式//当我们开始用上了 std::array 时,难免会遇到要将其兼容 C 风格的接口,这里有三种做法:void foo(int *p, int len) { return; } std::array<int 4> arr = {1,2,3,4}; // C 风格接口传参 // foo(arr, arr.size()); // 非法, 无法隐式转换 foo(&arr[0], arr.size()); foo(arr.data(), arr.size()); // 使用 `std::sort` std::sort(arr.begin(), arr.end());
(2)std::forward_list
C++11 引入了两组无序容器:
std::unordered_map/std::unordered_multimap 和 std::unordered_set/std::unordered_multiset。
元组的使用有三个核心的函数:
std::make_tuple: 构造元组
std::get: 获得元组某个位置的值
#include <tuple> #include <iostream> auto get_student(int id) { // 返回类型被推断为 std::tuple<double, char, std::string> if (id == 0) return std::make_tuple(3.8, 'A', "张三"); if (id == 1) return std::make_tuple(2.9, 'C', "李四"); if (id == 2) return std::make_tuple(1.7, 'D', "王五"); return std::make_tuple(0.0, 'D', "null"); // 如果只写 0 会出现推断错误, 编译失败 } int main() { auto student = get_student(0); std::cout << "ID: 0, " << "GPA: " << std::get<0>(student) << ", " << "成绩: " << std::get<1>(student) << ", " << "姓名: " << std::get<2>(student) << '\n'; double gpa; char grade; std::string name; // 元组进行拆包 std::tie(gpa, grade, name) = get_student(1); std::cout << "ID: 1, " << "GPA: " << gpa << ", " << "成绩: " << grade << ", " << "姓名: " << name << '\n'; } //合并两个元组,可以通过 std::tuple_cat 来实现。 auto new_tuple = std::tuple_cat(get_student(1), std::move(t));
11.正则表达式
(4)切割
12.语言级线程支持
std::thread
std::mutex/std::unique_lock
std::future/std::packaged_task
std::condition_variable
13.右值引用和移动语义,深拷贝和浅拷贝
纯虚函数的作用和实现方式
STL源码、vector、list、map、set
- 准模板库就是类与函数模板的大集合。STL共有6种组件:容器,容器适配器,迭代器,算法,函数对象和函数适配器
- vector封装数组,list封装了链表,map和set封装了二叉树。
- 序列容器:
(1)List封装了链表,Vector封装了数组, list和vector的最主要的区别在于vector使用连续内存存储的,他支持[]运算符,而list是以链表形式实现的,不支持[]。Vector对于随机访问的速度很快,但是对于插入尤其是在头部插入元素速度很慢,在尾部插入速度很快。List对于随机访问速度慢得多,因为可能要遍历整个链表才能做到,但是对于插入就快的多了,不需要拷贝和移动数据,只需要改变指针的指向就可以了。另外对于新添加的元素,Vector有一套算法,而List可以任意加入。
优点:(1) 不指定一块内存大小的数组的连续存储,即可以像数组一样操作,但可以对此数组进行动态操作。通常体现在push_back() pop_back()
(3) 节省空间。
缺点: (1) 在内部进行插入删除操作效率低。
(2) 只能在vector的最后进行push和pop,不能在vector的头进行push和pop。
(3) 当动态添加的数据超过vector默认分配的大小时要进行整体的重新分配、拷贝与释放
优点: (1) 不使用连续内存完成动态操作。
(2) 在内部方便的进行插入和删除操作
(3) 可在两端进行push、pop
缺点: (1) 不能进行内部的随机访问,即不支持[ ]操作符和vector.at()
(2) 相对于verctor占用内存多
优点: (1) 随机访问方便,即支持[ ]操作符和vector.at()
(2) 在内部方便的进行插入和删除操作
(3) 可在两端进行push、pop
缺点: (1) 占用内存多
2 如果你需要大量的插入和删除,而不关心随即存取,则应使用list
3 如果你需要随即存取,而且关心两端数据的插入和删除,则应使用deque
字节对齐的原则
- 从0位置开始存储;
- 变量存储的起始位置是该变量大小的整数倍;
- 结构体总的大小是其最大元素的整数倍,不足的后面要补齐;
- 结构体中包含结构体,从结构体中最大元素的整数倍开始存;
- 如果加入pragma pack(n) ,取n和变量自身大小较小的一个。
空结构体的sizeof()返回值
- 答案是1
静态连接与动态链接的区别
- 静态链接
所谓静态链接就是在编译链接时直接将需要的执行代码拷贝到调用处,优点就是在程序发布的时候就不需要依赖库,也就是不再需要带着库一块发布,程序可以独立执行,但是体积可能会相对大一些。 - 动态链接
所谓动态链接就是在编译的时候不直接拷贝可执行代码,而是通过记录一系列符号和参数,在程序运行或加载时将这些信息传递给操作系统,操作系统负责将需要的动态库加载到内存中,然后程序在运行到指定的代码时,去共享执行内存中已经加载的动态库可执行代码,最终达到运行时连接的目的。优点是多个程序可以共享同一段代码,而不需要在磁盘上存储多个拷贝,缺点是由于是运行时加载,可能会影响程序的前期执行性能。
重写、重载与隐藏的区别
重载的函数都是在类内的。只有参数类型或者参数个数不同,重载不关心返回值的类型。覆盖(重写)派生类中重新定义的函数,其函数名,返回值类型,参数列表都跟基类函数相同,并且基类函数前加了virtual关键字。
隐藏是指派生类的函数屏蔽了与其同名的基类函数,注意只要同名函数,不管参数列表是否相同,基类函数都会被隐藏。有两种情况:(1)参数列表不同,不管有无virtual关键字,都是隐藏;(2)参数列表相同,但是无virtual关键字,也是隐藏。
必须在构造函数初始化式里进行初始化的数据成员有哪些
1) 常量成员,因为常量只能初始化不能赋值,所以必须放在初始化列表里面2) 引用类型,引用必须在定义的时候初始化,并且不能重新赋值,所以也要写在初始化列表里面
3) 没有默认构造函数的类类型,因为使用初始化列表可以不必调用默认构造函数来初始化,而是直接调用拷贝构造函数初始化
C++四种类型转换
static_cast, dynamic_cast, const_cast, reinterpret_cast
- const_cast用于将const变量转为非const
- static_cast用的最多,对于各种隐式转换,非const转const,void*转指针等, static_cast能用于多态想上转化,如果向下转能成功但是不安全,结果未知;
- dynamic_cast用于动态类型转换。只能用于含有虚函数的类,用于类层次间的向上和向下转化。只能转指针或引用。向下转化时,如果是非法的对于指针返回NULL,对于引用抛异常。要深入了解内部转换的原理。
- reinterpret_cast几乎什么都可以转,比如将int转指针,可能会出问题,尽量少用;
- 为什么不使用C的强制转换?C的强制转换表面上看起来功能强大什么都能转,但是转化不够明确,不能进行错误检查,容易出错。
如何让一个类不能实例化?
- 将类定义为抽象基类或者将构造函数声明为private。
如何让main函数之前执行函数?
- C++中在main函数之前定义一个全局对象,调用构造函数。
- C语言中使用gcc的attribute关键字,声明constructor和destructor。
C++如何创建一个类,使得他只能在堆或者栈上创建?
- 只能在堆上生成对象:将析构函数设置为私有。
原因:C++是静态绑定语言,编译器管理栈上对象的生命周期,编译器在为类对象分配栈空间时,会先检查类的析构函数的访问性。若析构函数不可访问,则不能在栈上创建对象。 - 只能在栈上生成对象:将new 和 delete 重载为私有。
原因:在堆上生成对象,使用new关键词操作,其过程分为两阶段:第一阶段,使用new在堆上寻找可用内存,分配给对象;第二阶段,调用构造函数生成对象。将new操作设置为私有,那么第一阶段就无法完成,就不能够再堆上生成对象。