修正时间 08-16
复习时间 10-01
文章目录
- C/C++基础知识
- 1. static关键字的作用
- 2. C++和C的区别
- 3. C++中四种cast转换
- 4.C/C++中指针和引用的区别
- 5. C++中的智能指针
- 6. 数组和指针的区别
- 7. 野指针是什么
- 8. 为什么析构函数必须是虚函数?为什么c++默认的析构函数不是虚函数
- 9. 指针函数和函数指针(定义和初始化)
- 10. 析构函数(若有地址)
- 11. 静态函数和虚函数的区别
- 12. 重载和重写
- 13. strcpy和strlen
- 14. 静态多态和动态多态以及虚函数的实现
- 15. 内存的几个段,堆,栈
- 16. const限定符
- 17. C++函数栈空间的最大值
- 18. new/delete 和 malloc/free的区别
- 19.C++中拷贝构造函数的形参能否进行值传递
- 20. c语言是如何进行函数调用的
- 21. 结构体计算字节
- 22. c++ 结构体 和 class区别
- 23. 数组作为形参 sizeof结果
- 24. C语言是如何进行函数调用的
- 25. C++如何处理返回值
- 26. strlen 和 sizeof区别
- 27. 堆和栈的区别
- 28. 虚函数表存放在内存的什么区,虚表指针vptr的初始化时间
- 29. delete和delete[]区别
- 30. 宏定义和函数有何区别
- 31. 宏定义和typedef区别
- 32. 常量指针和指针常量区别
- 33. 野指针和悬空指针
- 34. 初始化和赋值的区别
- 35. 模板函数 函数模板 (作用)
- 36. 虚函数和纯虚函数的区别
- 37. 浅拷贝和深拷贝出现的问题
- 38. 内联函数和宏定义的区别
- 39. public,protected和private访问和继承权限/public/protected/private的区别?
- 40. 什么时候会调用拷贝构造函数
- 41. C++中NULL和nullptr区别
- 42. C++异常处理的方法(待补充)
- 43. 静态变量什么时候初始化
- 44. 值传递、指针传递、引用传递的区别和效率
- 45. malloc、realloc、calloc的区别
- 46. 什么事内存泄露,如何检测和避免
- 47. 析构函数和构造函数执行的顺序
- 48. C++函数调用的压栈过程(函数返回地址 函数参数 函数变量 ..上个含糊状态.下一个)(待补充)
- 49. 关于this指针你知道什么
- 50. 构造函数 拷贝构造函数 赋值运算符
- 51. C++中临时变量作为返回值的处理过程
- 52. 如何判断两个浮点数是否相等
- 53. 内存对齐的问题(有修正)
- 54. 如何利用重载比较结构体变量是否相等
- 55. 将字符串“hello world”从开始到打印到屏幕上的全过程?(待修正)
- 56. 模板类和模板函数的区别是什么?
- 57. 模板和实现可不可以不写在一个文件里面?为什么?(重复)
- 58. 如何在不使用额外空间的情况下,交换两个数?你有几种方法
- 59. 你知道strcpy和memcpy的区别是什么吗?
- 60. 程序在执行int main(int argc, char *argv[])时的内存结构,你了解吗?
- 61. 你知道空类的大小是多少吗?
- 62. 你什么情况用指针当参数,什么时候用引用,为什么?(待补充)
- 63. define宏定义和const的区别
- 64. C和C++类型安全(待补充)
- 65. volatile、mutable和explicit关键字的用法(待补充)
- 66. const关键字的作用
- 67. 什么是类的继承
- 68. new和delete的实现原理,delete如何知道释放内存的的大小
- 69. malloc申请的存储空间能用delete释放吗
- 70. 拷贝构造函数和赋值构造函数的区别
- 71. C++11有哪些新特性
- 72. C++中的指针参数传递和引用参数传递有什么区别?底层原理你知道吗?
- 73. define 和 typedef 和 inline
- 74. cout 和 printf的区别
- 75. 静态成员和普通成员的区别是什么
- 76. ifdef 和 endif代表着什么
- 77. 讲讲内联函数的理解?为什么不把所有的函数写成内联函数?
- 78. test.c 程序的编译过程
- 79 动态链接和静态链接
- 80. 友元函数和友元类的理解
- 80. 回调函数其他作用
- 81. static_cast比C语言中的转换强在哪里?
- 82. 如何阻止一个类被实例化?有哪些方法?
- 83. memcpy,strncpy
- 84. sprintf
- 85. 你知道const char* 与string之间的关系是什么
- 86. 类的初始化方式及其区别、效率
- 87. 哪些情况必须使用成员列表初始化
- 补充
C/C++基础知识
1. static关键字的作用
-
静态局部变量
- 内存中的存储位置:静态存储区.data,这导致未经初始化的会被自动初始化为0(
准确的说未被初始化和初始化为0的在bss零区 初始化为其他值的在data区也称为rw区
) - 作用域:作用域还是局部作用域,区别在于函数或者语句块结束的时候,局部静态变量并没有销毁,简单的说就是当该函数再次被调用的时候其值不变(或者说只初始化一遍,生存周期都是整个程序运行时期)。
- 内存中的存储位置:静态存储区.data,这导致未经初始化的会被自动初始化为0(
-
全局静态变量
- 内存中的存储位置:静态存储去.data,这导致未经初始化的会被自动初始化为0 。
准确的说未被初始化和初始化为0的在bss零区 初始化为其他值的在data区也称为rw区
) - 作用域:全局静态变量在声明它的文件之外是不可见的,准确的说从定义之处开始,到文件的结尾。
- 内存中的存储位置:静态存储去.data,这导致未经初始化的会被自动初始化为0 。
-
静态函数
- 仅在当前文件内有效。在不同的cpp文件中定义同名函数,不必担心命名冲突。对其它源文件隐藏。在现代C++中被无名namespace取代。
-
类的静态成员变量
- static修饰的数据成员属于类的组成部分,static修饰的数据成员不在栈上分配内存而在.data段分配内存,
static修饰的数据成员不能通过调用构造函数来进行初始化
,因此static修饰的数据成员必须在类外进行初始化且只会初始化一次。
- static修饰的数据成员属于类的组成部分,static修饰的数据成员不在栈上分配内存而在.data段分配内存,
-
类的静态成员函数
- 不能访问类的私有成员,只能访问类的static成员,不需要类的实例即可调用 《类名》::函数名(参数表) 。
- 属于整个类而非类的对象,没有this指针。
- 静态成员之间可以相互访问,包括静态成员函数访问静态数据成员和访问静态成员函数。非静态成员函数可以任意地访问静态成员函数和静态数据成员。静态成员函数不能访问非静态成员函数和非静态数据成员。
- 静态成员方法可以在类内或类外定义,但必须在类内声明
2. C++和C的区别
- 从设计思想上
- 一个是面向对象,一个是面向过程
- 语法上
- C++具有重载、继承、多态
- C++相比C增加需要类型安全的功能,比如强制类型转换
- C++ 模板类、函数模板
- 个人认为C++就是对C的升级
3. C++中四种cast转换
- 代码例子参考链接
- 简单的说
- const属性用const_cast。
- 基本类型转换用static_cast。
- 多态类之间的类型转换用daynamic_cast。
- 不同类型的指针类型转换用reinterpreter_cast。
4.C/C++中指针和引用的区别
- 指针就是地址指向一片内存区域,引用是别名
- 使用sizeof 指针大小是4,引用则是被引用对象的大小
- 指针可以被初始化为NULL,而引用初始化必须是一个已有对象
- 指针是间接操作,引用是直接操作,但是都是对对象值的直接修改
- 可以有const指针(确保不被修改),但没有const引用
- 指针在使用中可以指向其他对象,但是引用只能是一个对象的引用。
- 指针可以有多级,引用只有一级
- 指针和引用使用++运算符的意义不一样
- 如果返回动态内存分配的对象或者内存,必须使用指针,引用可能引起内存泄露
记忆 别名 大小 初始化 间接 const 多级 ++
5. C++中的智能指针
- 智能指针的作用就是管理一个指针,因为存在以下的情况:申请的空间在函数结束时忘记释放,造成内存泄露。使用智能指针很大程度避免这个问题,因为智能指针就是一个类,当超过类的作用域,类会自动调用析构函数,析构函数会自动释放资源。所以智能指针的作用原理就是函数结束时自动释放内存空间,不需要手动释放内存空间。
6. 数组和指针的区别
7. 野指针是什么
- 野指针就是指向一个已经被删除的对象或者未申请访问受限内存区域的指针。
当然有人区别野指针和悬浮指针,但是我觉的不是那么重要
8. 为什么析构函数必须是虚函数?为什么c++默认的析构函数不是虚函数
-
补充
:如果删除该基类的指针,就会调用该指针指向的派生类析构函数,而派生类的析构函数又自动调用基类的析构函数,这样整个派生类的对象完全被释放。 -
将可能被继承的父类的析构函数设置为虚函数,可以保证我们new一个子类,然后使用基类指针指向该子类对象,释放基类指针时可以释放掉子类的空间,防止内存泄露(
顺序 派生类 成员函数 基类
) -
C++默认的析构函数不是虚函数是因为虚函数需要额外的虚函数表和虚表指针,占用额外的内存。而对于不会被继承的类来说,其析构函数如果是虚函数,就会浪费内存。因此C++默认的析构函数不是虚函数,而是只有当需要当作父类时,设置为虚函数。
9. 指针函数和函数指针(定义和初始化)
- 指针函数,简单的来说,就是一个返回指针的函数,其本质是一个函数,而该函数的返回值是一个指针。
int *fun(int x,int y);
- 函数指针,其本质是一个指针变量,该指针指向这个函数。总结来说,函数指针就是指向函数的指针。
- 声明格式:类型说明符 (*函数名) (参数)
int (*fun)(int x,int y);
- 函数指针是需要把一个函数的地址赋值给它,有两种写法:
fun = &Function;
fun = Function;
- 取地址运算符&不是必需的,因为一个函数标识符就表示了它的地址,如果是函数调用,还必须包含一个圆括号括起来的参数表。
- 调用函数指针的方式也有两种:
x = (*fun)();
x = fun();
int add(int x,int y){
return x+y;
}
int sub(int x,int y){
return x-y;
}
//函数指针
int (*fun)(int x,int y);
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
//第一种写法
fun = add;
qDebug() << "(*fun)(1,2) = " << (*fun)(1,2) ;
//第二种写法
fun = ⊂
qDebug() << "(*fun)(5,3) = " << (*fun)(5,3) << fun(5,3);
return a.exec();
}
10. 析构函数(若有地址)
- 析构函数与构造函数对应,当对象结束其生命周期,如对象所在的函数已调用完毕时,系统会自动执行析构函数。
- 析构函数和类名相同只是多一个取反符号~。
- 区别于构造函数它不能带任何参数,
也没有返回值(包括void),只能有一个析构函数(可以多个构造),不能重载
。 - 系统有默认的析构函数
如果类中有指针,并且申请了空间,那么最好在析构函数中销毁
- 执行顺序:派生类-》对象成员-》基类
11. 静态函数和虚函数的区别
- 静态函数在编译的时候就已经确定运行时机,虚函数在运行的时候动态绑定。虚函数因为用了虚函数表机制,调用的时候会增加一次内存开销。
12. 重载和重写
- 重载:两个函数名相同,但是参数列表不同(个数,类型),返回值类型没有要求,在同一个作用域中。
- 重写:子类继承父类,父类中的函数是虚函数,在子类中重新定义这个虚函数,这个情况是重写。
13. strcpy和strlen
- strcpy是字符串拷贝函数
char* strcpy(char* dest,const char* src;)
src逐个拷贝到dest 遇到\0结束,因为没有指定长度,可能会造成拷贝越界,造成缓冲区溢出,安全版本是strncpy
- strlen是计算字符串长度的函数,返回从开始到\0之间的字符个数
14. 静态多态和动态多态以及虚函数的实现
- 多态的实现主要分为
静态多态和动态多态
,静态多态主要就是重载,在编译的时候就已经确定;动态多态使用虚函数机制实现的。举个例子:一个父类类型的指针指向一个子类对象,使用父类的指针去调用子类重写的父类中的虚函数的时候,就会调用子类重写过后的函数,在父类中声明为virtual关键字的函数,在子类中重写不需要加virtual也是虚函数
- 虚函数的实现:在有虚函数的类中,类的最开始部分就是虚函数表的指针,这个指针指向一个虚函数表,表中放虚函数的地址,实际的虚函数在代码段(.text)。当子类继承父类的时候也会继承其虚函数表,
当子类重写虚函数的时候,会把继承到的虚函数表中的地址替换为重写的函数地址
。使用了虚函数,会增加访问内存开销,降低效率。
15. 内存的几个段,堆,栈
16. const限定符
- 有时候我们希望定义这样一种变量,它的值不能被改变。为了满足这一要求,可以使用关键字const对变量的类型加以限定。
17. C++函数栈空间的最大值
- 默认是1M 不过可以调整
18. new/delete 和 malloc/free的区别
- new/delete是C++的关键字或者说运算符,而malloc/free是C的库函数, 后者必须指明申请的内存空间的大小并且需要对返回值void* 进行强制类型转换,对于类类型的对象,后者不会调用构造函数和析构函数。
补充说明
malloc仅仅分配内存空间,free仅仅回收空间,不具备调用构造函数和析构函数功能,用malloc分配空间存储类的对象存在风险;new和delete除了分配回收功能外,还会调用构造函数和析构函数
malloc和free返回的是void类型指针(必须进行类型转换),new和delete返回的是具体类型指针。
void* malloc(size_t size)//参数代表字节个数
void free(void* pointer)//参数代表内存地址
void func()
{
//开辟一个空间
int* p1=(int*)malloc(sizeof(int));
if(p1==NULL)
{
exit(1);
}
free(p1);
//开辟多个空间
int*p2=(int*)malloc(sizeof(int)*4);
if(p2==NULL)
{
exit(1);
}
free(p2);
void func()
{
//开辟一个空间
int* p1=new int(1);
delete p1;
//开辟多个空间
int*p2=new int[4];
delete []p2;
19.C++中拷贝构造函数的形参能否进行值传递
- 不能,调用拷贝构造函数的是否,首先就是将实参传递给形参,这个传递的时候又要调用拷贝构造函数。如此循环,无法完成拷贝,栈也会满。
20. c语言是如何进行函数调用的
-
需要明确 esp表示栈指针就是栈顶(低地址),ebp表示帧指针就是栈底(高低值),从高向低依次放入
当函数从入口函数main函数开始执行时,编译器会将我们
1操作系统的运行状态
,2main函数的返回地址
、3main的参数(右到左)
、4main函数中的变量
、进行依次压栈;当main函数开始调用func()函数时,编译器此时会将
5main函数的运行状态进行压栈
,再将func()函数的返回地址、func()函数的参数从右到左、func()定义变量依次压栈;当func()调用f()的时候,编译器此时会将func()函数的运行状态进行压栈,再将的返回地址、f()函数的参数从右到左、f()定义变量依次压栈
从代码的输出结果可以看出,函数f(var1)、f(var2)依次入栈,而后先执行f(var2),再执行f(var1),最后打印整个字符串,将栈中的变量依次弹出,最后主函数返回。
#include <iostream>
using namespace std;
int f(int n)
{
cout << n << endl;
return n;
}
void func(int param1, int param2)
{
int var1 = param1;
int var2 = param2;
printf("var1=%d,var2=%d", f(var1), f(var2));//如果将printf换为cout进行输出,输出结果则刚好相反
}
int main(int argc, char* argv[])
{
func(1, 2);
return 0;
}
//输出结果
//2
//1
//var1=1,var2=2
21. 结构体计算字节
- 简单的说就是
1、 分配内存的顺序是按照声明的顺序。
2、 每个变量相对于起始位置的偏移量必须是该变量类型大小的整数倍,不是整数倍空出内存,直到偏移量是整数倍为止。
3、 最后整个结构体的大小必须是里面变量类型最大值的整数倍。
22. c++ 结构体 和 class区别
-
在C语言中struct是只能定义数据成员,而不能定义成员函数的。而在C++中,struct类似于class,在其中既可以定义数据成员,又可以定义成员函数。
-
在C++中,struct与class基本是通用的,唯一不同的是如果使用class关键字,类中定义的成员变量或成员函数默认都是private属性的,而采用struct关键字,结构体中定义的成员变量或成员函数默认都是public属性的。
-
在C++中,没有抛弃C语言中的struct关键字,其意义就在于给C语言程序开发人员有一个归属感,并且能让C++编译器兼容以前用C语言开发出来的项目。
23. 数组作为形参 sizeof结果
- 数组作为形参,退化为指针,sizeof(指针)=4,因此不能再被调用函数中利用sizeof(数组名)/sizeof(数组类型)求数组的长度
24. C语言是如何进行函数调用的
- 上面说过了 main那边
25. C++如何处理返回值
- 首先,强调一点,和函数传参一样,函数返回时也会做一个拷贝。从某种角度上看,和传参一样,也分为三种:
- 返回值:返回任意类型的数据类型,会将返回数据做一个拷贝(副本)赋值给变量;由于需要拷贝,所以对于复杂对象这种方式效率比较低(调用对象的拷贝构造函数、析构函数);例如:int test(){}或者 Point test(){}
- 返回指针:返回一个指针,也叫指针类型的函数,在返回时只拷贝地址,对于对象不会调用拷贝构造函数和析构函数;例如:int *test(){} 或者 Point *test(){}
- 返回引用:返回一个引用,也叫引用类型的函数,在返回时只拷贝地址,对于对象不会调用拷贝构造函数和析构函数;例如:int &test(){}或者 Point &test(){}
一般来说,在函数内对于存在栈上的局部变量的作用域只在函数内部,在函数返回后,局部变量的内存会自动释放。因此,如果函数返回的是局部变量的值,不涉及地址,程序不会出错;但是如果返回的是局部变量的地址(指针)的话,就会造成野指针,程序运行会出错。因为函数只是把指针复制后返回了,但是指针指向的内容已经被释放,这样指针指向的内容就是不可预料,调用就会出错。
26. strlen 和 sizeof区别
-
sizeof是运算符,并不是函数,结果在编译时得到而非运行中获得;strlen是字符处理的库函数。
-
sizeof参数可以是任何数据的类型或者数据(sizeof参数不退化);strlen的参数只能是字符指针且结尾是’\0’的字符串。
-
因为sizeof值在编译时确定,所以不能用来得到动态分配(运行时分配)存储空间的大小。
int main(int argc, char const *argv[]){
const char* str = "name";
sizeof(str); // 取的是指针str的长度,是4
strlen(str); // 取的是这个字符串的长度,不包含结尾的 \0。大小是4
return 0;
}
补充
提到sizeof(地址)的值为8,是在64位的编译环境下的,指针的占用大小为8字节;而在32位环境下,指针占用大小为4字节。
27. 堆和栈的区别
- 申请方式不同
- 栈由系统自动分配
- 堆自己申请和释放
- 申请大小限定不同
- 栈顶和栈底是之前预设好的,栈是向栈底(低地址)扩展,大小固定,可以通过ulimit -a查看,由ulimit -s修改。
- 堆向高地址扩展(向上),是不连续的内存区域,大小可以灵活调整。
- 栈空间默认是4M, 堆区一般是 1G - 4G
- 申请效率不同。
- 栈由系统分配,速度快,不会有碎片。
- 堆由程序员分配,速度慢,且会有碎片。
因为操作系统会在底层对栈提供支持,会分配专门的寄存器存放栈的地址,栈的入栈出栈操作也十分简单,并且有专门的指令执行,所以栈的效率比较高也比较快。
而堆的操作是由C/C++函数库提供的,在分配堆内存的时候需要一定的算法寻找合适大小的内存。并且获取堆的内容需要两次访问,第一次访问指针,第二次根据指针保存的地址访问内存,因此堆比较慢。
28. 虚函数表存放在内存的什么区,虚表指针vptr的初始化时间
- C++中虚函数表位于只读数据段(.rodata),也就是C++内存模型中的常量区;而虚函数则位于代码段(.text),也就是C++内存模型中的代码区。
29. delete和delete[]区别
- 简单的说就是
- delete 释放new分配的单个对象指针指向的内存
- delete[] 释放new分配的对象数组指针指向的内存
- 对于像int/char/long/int*/struct等等简单数据类型,由于对象没有destructor,所以用delete 和delete [] 是一样的!但是如果是C++对象数组就不同了!
30. 宏定义和函数有何区别
-
宏在编译之前完成替换(预处理),之后被替换的文本参与编译,相当于直接插入了代码,运行时不存在函数调用,执行起来更快;函数调用在运行时需要跳转到具体调用函数。
-
宏定义属于在结构中插入代码,没有返回值;函数调用具有返回值。
-
宏定义参数没有类型,不进行类型检查;函数参数具有类型,需要检查类型。
-
宏定义不要在最后加分号。
31. 宏定义和typedef区别
-
宏主要用于定义常量及书写复杂的内容;typedef主要用于定义类型别名。
-
宏替换发生在编译阶段之前,属于文本插入替换;typedef是编译的一部分。
-
宏不检查类型;typedef会检查数据类型。
-
宏不是语句,不在在最后加分号;typedef是语句,要加分号标识结束。
-
注意对指针的操作,typedef char * p_char和#define p_char char *区别巨大。
32. 常量指针和指针常量区别
-
指针常量是一个指针,读成常量的指针,指向一个只读变量,也就是后面所指明的int const 和 const int,都是一个常量,可以写作int const *p或const int *p。
-
常量指针是一个不能给改变指向的指针。指针是个常量,必须初始化,一旦初始化完成,它的值(也就是存放在指针中的地址)就不能在改变了,即不能中途改变指向,如int *const p。
-
2021-09-14 修正
-
指针常量 (先写指针再常量,常量操作的是变量,所有代表变量指向不可变):
int * const p //指针常量
int a,b;
int * const p=&a //指针常量
//那么分为一下两种操作
*p=9;//操作成功
p=&b;//操作错误
- 常量指针(先常量 后指针,表示常量修饰的是指针 表示指针指向的内存区域内容不可换):
const int *p = &a; //常量指针
int a,b;
const int *p=&a //常量指针
//那么分为一下两种操作
*p=9;//操作错误
p=&b;//操作成功
33. 野指针和悬空指针
- 野指针:指的是没有被初始化过的指针
- 悬浮指针:最初指向的内存已经被释放了的一种指针
当然糙一点 统称为野指针也行
int* p; // 未初始化
std::cout<< *p << std::endl; // 未初始化就被用
//----------------------------
int * p = nullptr;
int* p2 = new int;
p = p2;
delete p2;
34. 初始化和赋值的区别
class A{
public:
int num1;
int num2;
public:
A(int a=0, int b=0):num1(a),num2(b){};
A(const A& a){};
//重载 = 号操作符函数
A& operator=(const A& a){
num1 = a.num1 + 1;
num2 = a.num2 + 1;
return *this;
};
};
int main(){
A a(1,1);
A a1 = a; //拷贝初始化操作,调用拷贝构造函数
A b;
b = a;//赋值操作,对象a中,num1 = 1,num2 = 1;对象b中,num1 = 2,num2 = 2
return 0;
}
35. 模板函数 函数模板 (作用)
- 模板函数的作用是定义一个通用类型的函数,以便更好的复用。
36. 虚函数和纯虚函数的区别
-
类如果声明了虚函数,这个函数是实现了的,即使是空实现,它的作用就是为了能让这个函数在它的子类里面可以被覆盖,这样编译器就可以使用动态绑定来达到多态的目的(即父类指针指向子类对象,调用子类方法)。而纯虚函数只是在基类中的一个函数定义,即是一个函数声明而已,具体的实现需要留到子类当中。
-
虚函数在子类里面也可以不进行重写(只有虚方法和抽象方法才能够被重写);但纯虚函数必须在子类去实现。
-
虚函数的类用于“实作继承”,也就是说继承接口的同时也继承了父类的实现。当然,子类也可以进行覆写,从而完成自己关于此函数的实现。纯虚函数的类用于“介面继承”,即纯虚函数关注的是接口的统一性,实现由子类去完成。
-
带纯虚函数的类叫做抽象类,这种类不能直接生成对象,而只有被继承,并重写其虚函数后,才能使用。
#include <iostream>
using namespace std;
// 抽象类
class Base {
public:
virtual void func() = 0; // 纯虚函数
};
class child1 : public Base {
public:
void func() { cout << "it's child 1" << endl; } // 覆写父类的纯虚函数
};
class child2 : public Base {
public:
void func() { cout << "it's child 2" << endl; } // 覆写父类的纯虚函数
};
int main() {
// Base b; // 纯虚函数无法实例化
Base* b = nullptr; // 父类指针
child1 c1; // 子类对象
child2 c2; // 子类对象
b = &c1;
b->func(); // 多态
b = &c2;
b->func(); // 多态
return 0;
}
37. 浅拷贝和深拷贝出现的问题
[STL]深拷贝和浅拷贝问题(内存泄露+内存未释放+调用拷贝构造的五种情况)
- 之前写过专门的一篇
38. 内联函数和宏定义的区别
关于什么是内联函数后面还有
- 在使用时,宏只做简单字符串替换(编译前)。而内联函数可以进行参数类型检查(编译时),且具有返回值。
- 内联函数在编译时直接将函数代码嵌入到目标代码中,省去函数调用的开销来提高执行效率,并且进行参数类型检查,具有返回值,可以实现重载。
- 宏定义时要注意书写(参数要括起来)否则容易出现歧义,内联函数不会产生歧义
- 内联函数有类型检测、语法判断等功能,而宏没有
使用宏定义的地方都可以使用 inline 函数。
作为类成员接口函数来读写类的私有成员或者保护成员,会提高效率。
39. public,protected和private访问和继承权限/public/protected/private的区别?
-
public的变量和函数在类的内部外部都可以访问。
-
protected的变量和函数只能在类的内部和其派生类中访问。
-
private修饰的元素只能在类内访问。
2021-09-13
补充
对于成员的访问,一般来说有类内访问和类外访问。类内访问就是类里面的其它成员直接使用成员名进行访问。类外访问,意思是通过形如对象名.成员名的方式进行访问。此时的.可以理解为中文的“的”。当然,->也有相同的含义。所以 单例饿汉模式中 私有化对象 可以再类外 =new 就可以调用构造函数
40. 什么时候会调用拷贝构造函数
- 用类的一个实例化对象去初始化另一个对象的时候
- 函数的参数是类的对象时(非引用传递)
- 函数的返回值是函数体内局部对象的类的对象时 ,此时虽然发生(Named return Value优化)NRV优化,但是由于返回方式是值传递,所以会在返回值的地方调用拷贝构造函数
- 补充一个 push_back
41. C++中NULL和nullptr区别
- NULL在C中就是空指针,但是在C++中void* 类型是不允许隐式转换成其他类型的。所以,C++11加入了nullptr,可以保证在任何情况下都代表空指针,而不会出现上述的情况,
因此,建议以后还是都用nullptr替代NULL吧,而NULL就当做0使用
。
42. C++异常处理的方法(待补充)
- try、throw和catch关键字
- 函数的异常声明列表
- C++标准异常类 exception
43. 静态变量什么时候初始化
-
- 初始化只有一次,但是可以多次赋值,在主程序之前,编译器已经为其分配好了内存。
-
- 静态局部变量和全局变量一样,数据都存放在全局区域,所以在主程序之前,编译器已经为其分配好了内存,但在C和C++中静态局部变量的初始化节点又有点不太一样
。在C中,初始化发生在代码执行之前,编译阶段分配好内存之后,就会进行初始化,所以我们看到在C语言中无法使用变量对静态局部变量进行初始化
,在程序运行结束,变量所处的全局内存会被全部回收。
- 静态局部变量和全局变量一样,数据都存放在全局区域,所以在主程序之前,编译器已经为其分配好了内存,但在C和C++中静态局部变量的初始化节点又有点不太一样
而在C++中,初始化时在执行相关代码时才会进行初始化,
主要是由于C++引入对象后,要进行初始化必须执行相应构造函数和析构函数,在构造函数或析构函数中经常会需要进行某些程序中需要进行的特定操作,并非简单地分配内存。所以C++标准定为全局或静态对象是有首次用到时才会进行构造,并通过atexit()来管理。在程序结束,按照构造顺序反方向进行逐个析构。所以在C++中是可以使用变量对静态局部变量进行初始化的
。
44. 值传递、指针传递、引用传递的区别和效率
-
- 值传递:有一个形参向函数所属的栈拷贝数据的过程,如果值传递的对象是类对象 或是大的结构体对象,将耗费一定的时间和空间。(传值)
-
- 指针传递:同样有一个形参向函数所属的栈拷贝数据的过程,但拷贝的数据是一个固定为4字节的地址。(传值,传递的是地址值)
-
- 引用传递:同样有上述的数据拷贝过程,但其是针对地址的,相当于为该数据所在的地址起了一个别名。(传地址)
-
- 效率上讲,指针传递和引用传递比值传递效率高。一般主张使用引用传递,代码逻辑上更加紧凑、清晰。
45. malloc、realloc、calloc的区别
void* malloc(unsigned int num_size);
int *p = (int *)malloc(20*sizeof(int));申请20个int类型的空间;
//calloc 省去人为计算的时间 malloc申请的空间的值是随机初始化的,calloc申请的空间的值是初始化为0的;
void* calloc(size_t n,size_t size);
int *p = calloc(20, sizeof(int));
//给动态分配的空间分配额外的空间,用于扩充容量。
void realloc(void *p, size_t new_size);
46. 什么事内存泄露,如何检测和避免
补充 区别一下野指针和悬浮指针
-
一般我们常说的内存泄漏是指堆内存的泄漏。堆内存是指程序从堆中分配的,大小任意的(内存块的大小可以在程序运行期决定)内存块,使用完后必须显式释放的内存。应用程序般使用malloc,、realloc、 new等函数从堆中分配到块内存,使用完后,程序必须负责相应的调用free或delete释放该内存块,否则,这块内存就不能被再次使用,我们就说这块内存泄漏了
-
如何避免
- 计数法:使用new或者malloc时,让该数+1,delete或free时,该数-1,程序执行完打印这个计数,如果不为0则表示存在内存泄露
- 一定要将基类的析构函数声明为虚函数
- 对象数组的释放一定要用delete []
- 有new就有delete,有malloc就有free,保证它们一
47. 析构函数和构造函数执行的顺序
- 构造函数:基类构造函数、对象成员构造函数、派生类本身的构造函数
- 析构函数相反
48. C++函数调用的压栈过程(函数返回地址 函数参数 函数变量 …上个含糊状态.下一个)(待补充)
- 前面也说过一次了
49. 关于this指针你知道什么
- this指针实质上是一个函数参数,只是编译器隐藏形式的、语法层面上的参数。
class A
{
public:
int func(int p) {};
};
//其中,函数func的原型在编译器看来应该是:
int func(A* const this, int p) {};
-
this指针在成员函数开始前构造,在成员函数结束后清除。
-
this指针并不占用对象的空间
-
this在成员函数开始执行前构造,在成员执行结束后清除。
-
this指针会因编译器不同而有不同的放置位置。可能是堆、栈,也可能是寄存器。C++是一种静态的语言,那么对C++的分析应该从语法层面和实现层面两个方面进行。
-
this指针只有在成员函数中オ有定义。因此,你获得一个对象后,也不能通过对象使用this指针。所以,我们无法知道一个对象的this指针的位置(只有在成员函数里才有this指针的位置)。当然,在成员函数里,你是可以知道this指针的位置的(可以通过&this获得),也可以直接使用它。
简单回答如下 -
this指针是类的指针,指向对象的首地址。
-
this指针只能在成员函数中使用,在全局函数、静态成员函数中都不能用this。
-
this指针只有在成员函数中才有定义,且存储位置会因编译器不同有不同存储位置。
50. 构造函数 拷贝构造函数 赋值运算符
构造函数
对象不存在,没用别的对象初始化,在创建一个新的对象时调用构造函数
拷贝构造函数
对象不存在,但是使用别的已经存在的对象来进行初始化
赋值运算符
对象存在,用别的对象给它赋值,这属于重载“=”号运算符的范畴,“=”号两侧的对象都是已存在的
#include <iostream>
using namespace std;
class A
{
public:
A()
{
cout << "我是构造函数" << endl;
}
A(const A& a)
{
cout << "我是拷贝构造函数" << endl;
}
A& operator = (A& a)
{
cout << "我是赋值操作符" << endl;
return *this;
}
~A() {};
};
int main()
{
A a1; //调用构造函数
A a2 = a1; //调用拷贝构造函数
a2 = a1; //调用赋值操作符
return 0;
}
//输出结果
//我是构造函数
//我是拷贝构造函数
//我是赋值操作符
51. C++中临时变量作为返回值的处理过程
当函数退出时,临时变量出栈,即临时变量已经被销毁,临时变量占用的内存空间没有被清空,但是已经可以被分配给其他变量了,所以有可能在函数退出时,该内存已经被修改了,对于临时变量来说已经是没有意义的值了。
C语言里规定:16bit程序中,返回值保存在ax寄存器中,32bit程序中,返回值保持在eax寄存器中,如果是64bit返回值,edx寄存器保存高32bit,eax寄存器保存低32bit。
- 由此可见,函数调用结束后,返回值被临时存储到寄存器中,并没有放到堆或栈中,也就是说与内存没有关系了。当退出函数的时候,临时变量可能被销毁,但是返回值却被放到寄存器中与临时变量的生命周期没有关系。如果我们需要返回值,一般使用赋值语句就可以了。A a = func();
52. 如何判断两个浮点数是否相等
对两个浮点数判断大小和是否相等不能直接用==来判断,会出错!明明相等的两个数比较反而是不相等!对于两个浮点数比较只能通过相减并与预先设定的精度比较,记得要取绝对值!浮点数与0的比较也应该注意。与浮点数的表示方式有关。
53. 内存对齐的问题(有修正)
1、 分配内存的顺序是按照声明的顺序。
2、 每个变量相对于起始位置的偏移量必须是该变量类型大小的整数倍,不是整数倍空出内存,直到偏移量是整数倍为止。
3、 最后整个结构体的大小必须是里面变量类型最大值的整数倍。
添加了#pragma pack(n)后规则就变成了下面这样:
1、 偏移量要是n和当前变量大小中较小值的整数倍
2、 整体大小要是n和最大变量大小中较小值的整数倍
3、 n值必须为1,2,4,8…,为其他值时就按照默认的分配规则
54. 如何利用重载比较结构体变量是否相等
struct foo {
int a;
int b;
bool operator==(const foo& rhs) *//* *操作运算符重载*
{
return( a == rhs.a) && (b == rhs.b);
}
};
- 元素的话,一个个比
- 指针直接比较,如果保存的是同一个实例地址,则(p1==p2)为真;
55. 将字符串“hello world”从开始到打印到屏幕上的全过程?(待修正)
1.用户告诉操作系统执行HelloWorld程序(通过键盘输入等)
2.操作系统:找到helloworld程序的相关信息,检查其类型是否是可执行文件;并通过程序首部信息,确定代码和数据在可执行文件中的位置并计算出对应的磁盘块地址。
3.操作系统:创建一个新进程,将HelloWorld可执行文件映射到该进程结构,表示由该进程执行helloworld程序。
4.操作系统:为helloworld程序设置cpu上下文环境,并跳到程序开始处。
5.执行helloworld程序的第一条指令,发生缺页异常
6.操作系统:分配一页物理内存,并将代码从磁盘读入内存,然后继续执行helloworld程序
7.helloword程序执行puts函数(系统调用),在显示器上写一字符串
8.操作系统:找到要将字符串送往的显示设备,通常设备是由一个进程控制的,所以,操作系统将要写的字符串送给该进程
9.操作系统:控制设备的进程告诉设备的窗口系统,它要显示该字符串,窗口系统确定这是一个合法的操作,然后将字符串转换成像素,将像素写入设备的存储映像区
10.视频硬件将像素转换成显示器可接收和一组控制数据信号
11.显示器解释信号,激发液晶屏
12.OK,我们在屏幕上看到了HelloWorld
56. 模板类和模板函数的区别是什么?
57. 模板和实现可不可以不写在一个文件里面?为什么?(重复)
https://www.zhihu.com/question/270627626/answer/383917556
面这段话的意思就是说,只有模板实例化时,编译器才会得知T实参是什么。编译器在处理模板实例化时,不仅仅要看到模板的定义式,还需要模版的实现体。比如说存在模板CTest, 其类定义式写在CTest.h,类的实现体写在CTest.cpp中。对于模板来说,编译器在处理CTest.cpp文件时,编译器无法预知T的实参是什么,所以编译器对其实现是不作处理的(即CTest.obj中没有为编译器为实现体生成的对应的二进制代码)。现在有main.cpp真正使用了该模板(比方说,生成模板类的一个对象,并调用其函数),如果定义和实现分离,则编译器可以根据定义式生成模板类的对象(因为此处仅仅需要定义式就知道该对象在内存中需要多少空间并进一步分配了),但是调用对象的函数(即真正使用)需要该函数的定义,由于main.cpp仅仅include了模板的声明(所以只能找到该函数的声明),所以无法找到该函数的定义,此时编译器会寄希望于链接器在其他obj文件(这里就是指CTest.obj文件)中寻找该模板的实现体,但是就像之前说过的,CTest.obj中也没有实现体生成的二进制代码。如果定义和实现是在同一个文件(比如说CTest.h)中,那么编译器在编译时就可以寻找到模板的实现体。这里看下面的三个例子。
58. 如何在不使用额外空间的情况下,交换两个数?你有几种方法
1) 算术
x = x + y;
y = x - y;
x = x - y;
2) 异或
x = x^y;// 只能对int,char..
y = x^y;
x = x^y;
x ^= y ^= x;
59. 你知道strcpy和memcpy的区别是什么吗?
void *memcpy(void *dest, const void *src, int n);从源src所指的内存地址的起始位置开始拷贝n个字节到目标dest所指的内存地址的起始位置中
- 复制的内容不同。strcpy只能复制字符串,而memcpy可以复制任意内容,例如字符数组、整型、结构体、类等。
- 复制的方法不同。strcpy不需要指定长度,它遇到被复制字符的串结束符"\0"才结束,所以容易溢出。memcpy则是根据其第3个参数决定复制的长度。
- 用途不同。通常在复制字符串时用strcpy,而需要复制其他类型数据时则一般用memcpy
60. 程序在执行int main(int argc, char *argv[])时的内存结构,你了解吗?
-
参数的含义是程序在命令行下运行的时候,需要输入argc 个参数,每个参数是以char 类型输入的,依次存在数组里面,数组是 argv[],所有的参数在指针
-
char * 指向的内存中,数组的中元素的个数为 argc 个,第一个参数为程序的名称。
- argv[0]指向程序运行的全路径名
- argv[1]指向在DOS命令行中执行程序名后的第一个字符串
- argv[2]指向执行程序名后的第二个字符串
- argv[3]指向执行程序名后的第三个字符串
- argv[argc]为NULL
- argv[0]指向程序运行的全路径名
61. 你知道空类的大小是多少吗?
-
C++空类的大小不为0,不同编译器设置不一样,vs设置为1;
-
C++标准指出,不允许一个对象(当然包括类对象)的大小为0,不同的对象不能具有相同的地址;
-
带有虚函数的C++类大小不为1,因为每一个对象会有一个vptr指向虚函数表,具体大小根据指针大小确定;
-
C++中要求对于类的每个实例都必须有独一无二的地址,那么编译器自动为空类分配一个字节大小,这样便保证了每个实例均有独一无二的内存地址。
62. 你什么情况用指针当参数,什么时候用引用,为什么?(待补充)
- 使用引用参数的主要原因有两个:
程序员能修改调用函数中的数据对象
通过传递引用而不是整个数据–对象,可以提高程序的运行速度
- 一般的原则:
对于使用引用的值而不做修改的函数:
如果数据对象很小,如内置数据类型或者小型结构,则按照值传递;
如果数据对象是数组,则使用指针(唯一的选择),并且指针声明为指向const的指针;
如果数据对象是较大的结构,则使用const指针或者引用,已提高程序的效率。这样可以节省结构所需的时间和空间;
如果数据对象是类对象,则使用const引用(传递类对象参数的标准方式是按照引用传递);
- 对于修改函数中数据的函数:
如果数据是内置数据类型,则使用指针
如果数据对象是数组,则只能使用指针
如果数据对象是结构,则使用引用或者指针
如果数据是类对象,则使用引用
63. define宏定义和const的区别
-
编译阶段
- define是在编译的预处理阶段起作用,而const是在编译、运行的时候起作用
-
安全性
- define只做替换,不做类型检查和计算,也不求解,容易产生错误,一般最好加上一个大括号包含住全部的内容,要不然很容易出错
const常量有数据类型,编译器可以对其进行类型安全检查
- define只做替换,不做类型检查和计算,也不求解,容易产生错误,一般最好加上一个大括号包含住全部的内容,要不然很容易出错
-
内存占用
- define只是将宏名称进行替换,在内存中会产生多分相同的备份。const在程序运行中只有一份备份,且可以执行常量折叠,能将复杂的的表达式计算出结果放入常量表
64. C和C++类型安全(待补充)
65. volatile、mutable和explicit关键字的用法(待补充)
66. 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类型变量可以通过
类型转换符const_cast
将const类型转换为非const类型;
- const类型变量可以通过
-
- const类型变量必须定义的时候进行初始化,因此也导致如果类的成员变量有const类型的变量,那么该变量必须在类的初始化列表中进行初始化;
-
- 对于函数值传递的情况,因为参数传递是通过复制实参创建一个临时变量传递进函数的,函数内只能改变临时变量,但无法改变实参。则这个时候无论加不加
const对实参不会产生任何影响。但是在引用或指针传递函数调用中,因为传进去的是一个引用或指针,这样函数内部可以改变引用或指针所指向的变量,这时const 才是实实在在地保护了实参所指向的变量。
因为在编译阶段编译器对调用函数的选择是根据实参进行的,所以,只有引用传递和指针传递可以用是否加const来重载。一个拥有顶层const的形参无法和另一个没有顶层const的形参区分开来。
- 对于函数值传递的情况,因为参数传递是通过复制实参创建一个临时变量传递进函数的,函数内只能改变临时变量,但无法改变实参。则这个时候无论加不加
67. 什么是类的继承
-
- 类与类之间的关系
-
has-A包含关系,用以描述一个类由多个部件类构成,实现has-A关系用类的成员属性表示,即一个类的成员属性是另一个已经定义好的类;
-
use-A,一个类使用另一个类,通过类之间的成员函数相互联系,定义友元或者通过传递参数的方式来实现;
-
is-A,继承关系,关系具有传递性;
-
- 继承的相关概念
- 所谓的继承就是一个类继承了另一个类的属性和方法,这个新的类包含了上一个类的属性和方法,被称为子类或者派生类,被继承的类称为父类或者基类;
-
- 继承的特点
- 子类拥有父类的所有属性和方法,子类可以拥有父类没有的属性和方法,子类对象可以当做父类对象使用;
-
- 继承中的访问控制
- public、protected、private
-
- 继承中的构造和析构函数
-
- 继承中的兼容性原则
68. new和delete的实现原理,delete如何知道释放内存的的大小
- 对于简单类型 直接调用operator new分配内存。而对于复杂结构,先调用operator new分配内存,然后在分配的内存上调用构造函数
- 对于简单类型,new[]计算好大小后调用operator new,对于复杂数据结构,
new[]先调用operator new[]分配内存,然后在p的前四个字节写入数组大小n
,然后调用n次构造函数,针对复杂类型,new[]会额外存储数组大小;- new表达式调用一个名为operator new(operator new[])函数,分配一块足够大的、原始的、未命名的内存空间;
- 编译器运行相应的构造函数以构造这些对象,并为其传入初始值;
- 对象被分配了空间并构造完成,返回一个指向该对象的指针。
- delete简单数据类型默认只是调用free函数;复杂数据类型先调用析构函数再调用operator delete;
针对简单类型,delete和delete[]等同
。假设指针p指向new[]分配的内存。因为要4字节存储数组大小,实际分配的内存地址为[p-4],系统记录的也是这个地址。delete[]实际释放的就是p-4指向的内存。而delete会直接释放p指向的内存,这个内存根本没有被系统记录,所以会崩溃。
需要在 new [] 一个对象数组时,需要保存数组的维度,C++ 的做法是在分配数组空间时多分配了 4 个字节的大小,专门保存数组的大小,在 delete [] 时就可以取出这个保存的数,就知道了需要调用析构函数多少次了。
所以我们常常简单的说 使用new 对应delete new【】 对应使用delete【】
69. malloc申请的存储空间能用delete释放吗
- 不能,malloc /free(库函数)主要为了兼容C,new和delete(运算符 关键字) 完全可以取代malloc /free的。
- malloc /free的操作对象都是必须明确大小的,而且不能用在动态类上。
- new 和delete会自动进行类型检查和大小,malloc/free不能执行构造函数与析构函数,所以动态对象它是不行的。
- 当然从理论上说使用malloc申请的内存是可以通过delete释放的。不过一般不这样写的。而且也不能保证每个C++的运行时都能正常。
70. 拷贝构造函数和赋值构造函数的区别
-
拷贝构造函数是函数,赋值运算符是运算符重载。
-
拷贝构造函数会生成新的类对象,赋值运算符不能。
-
拷贝构造函数是直接构造一个新的类对象,所以在初始化对象前不需要检查源对象和新建对象是否相同;赋值运算符需要上述操作并提供两套不同的复制策略,另外赋值运算符中如果原来的对象有内存分配则需要先把内存释放掉。
-
形参传递是调用拷贝构造函数(调用的被赋值对象的拷贝构造函数),但并不是所有出现"="的地方都是使用赋值运算符,如下:
Student s;
Student s1 = s; // 调用拷贝构造函数
Student s2;
s2 = s; // 赋值运算符操作
补充:平时常用:类中有指针变量时要重写析构函数、拷贝构造函数和赋值运算符
71. C++11有哪些新特性
- nullptr替代 NULL
- 引入了 auto 和 decltype 这两个关键字实现了类型推导
- 基于范围的 for 循环for(auto& i : res){}
- 类和结构体的中初始化列表
- Lambda 表达式(匿名函数)
- std::forward_list(单向链表)
- 右值引用和move语义
72. C++中的指针参数传递和引用参数传递有什么区别?底层原理你知道吗?
1) 指针参数传递本质上是值传递,它所传递的是一个地址值。
值传递过程中,被调函数的形式参数作为被调函数的局部变量处理,会在栈中开辟内存空间以存放由主调函数传递进来的实参值,从而形成了实参的一个副本
(替身)。
值传递的特点是,被调函数对形式参数的任何操作都是作为局部变量进行的,不会影响主调函数的实参变量的值(形参指针变了,实参指针不会变
)。
2) 引用参数传递过程中,被调函数的形式参数也作为局部变量在栈中开辟了内存空间,但是这时存放的是由主调函数放进来的实参变量的地址。
被调函数对形参(本体)的任何操作都被处理成间接寻址,即通过栈中存放的地址访问主调函数中的实参变量(根据别名找到主调函数中的本体)。
因此,被调函数对形参的任何操作都会影响主调函数中的实参变量。
3) 引用传递和指针传递是不同的,虽然他们都是在被调函数栈空间上的一个局部变量,但是任何对于引用参数的处理都会通过一个间接寻址的方式操作到主调函数中的相关变量。
而对于指针传递的参数,如果改变被调函数中的指针地址,它将应用不到主调函数的相关变量。如果想通过指针参数传递来改变主调函数中的相关变量(地址),那就得使用指向指针的指针或者指针引用。
4) 从编译的角度来讲,程序在编译时分别将指针和引用添加到符号表上,符号表中记录的是变量名及变量所对应地址。
指针变量在符号表上对应的地址值为指针变量的地址值,而引用在符号表上对应的地址值为引用对象的地址值(与实参名字不同,地址相同)。
符号表生成之后就不会再改,因此指针可以改变其指向的对象(指针变量中的值可以改),而引用对象则不能修改。
73. define 和 typedef 和 inline
-
define和别名typedef的区别
-
- 执行时间不同,typedef在编译阶段有效,typedef有类型检查的功能;#define是宏定义,发生在预处理阶段,不进行类型检查;
-
- 功能差异,typedef用来定义类型的别名,定义与平台无关的数据类型,与struct的结合使用等。#define不只是可以为类型取别名,还可以定义常量、变量、编译开关等。
-
- 作用域不同,#define没有作用域的限制,只要是之前预定义过的宏,在以后的程序中都可以使用。而typedef有自己的作用域。
-
-
define与inline的区别
-
- #define是关键字,inline是函数;
-
- 宏定义在预处理阶段进行文本替换,inline函数在编译阶段进行替换;
-
- inline函数有类型检查,相比宏定义比较安全;
-
74. cout 和 printf的区别
- cout<<是一个函数,cout<<后可以跟不同的类型是因为cout<<已存在针对各种类型数据的重载,所以会自动识别数据的类型。输出过程会首先将输出字符放入缓冲区,然后输出到屏幕。
cout < < "abc " < <endl;
或cout < < "abc\n ";cout < <flush; 这两个才是一样的.
- flush立即强迫缓冲输出。 printf是无缓冲输出。有输出时立即输出
75. 静态成员和普通成员的区别是什么
- 生命周期
静态成员变量从类被加载开始到类被卸载,一直存在;
普通成员变量只有在类创建对象后才开始存在,对象结束,它的生命期结束;
- 共享方式
静态成员变量是全类共享;普通成员变量是每个对象单独享用的;
- 定义位置
普通成员变量存储在栈或堆中,而静态成员变量存储在静态全局区;
- 初始化位置
普通成员变量在类中初始化;静态成员变量在类外初始化;
- 默认实参
可以使用静态成员变量作为默认实参,
76. ifdef 和 endif代表着什么
- 一般情况下,源程序中所有的行都参加编译。但是有时希望对其中一部分内容只在满足一定条件才进行编译,也就是对一部分内容指定编译的条件,这就是“条件编译”。有时,希望当满足某条件时对一组语句进行编译,而当条件不满足时则编译另一组语句。
- 条件编译命令最常见的形式为:
\#ifdef 标识符
程序段1
\#else
程序段2
\#endif
- 它的作用是:当标识符已经被定义过(一般是用#define命令定义),则对程序段1进行编译,否则编译程序段2。 其中#else部分也可以没有,即:
在一个大的软件工程里面,可能会有多个文件同时包含一个头文件,当这些文件编译链接成一个可执行文件上时,就会出现大量“重定义”错误。在头文件中使用#define、#ifndef、#ifdef、#endif能避免头文件重定义。
待补充
- 当程序中有函数重载时,函数的匹配原则和顺序是什么?
10.01补充 汇编生成目标文件 链接串起来生成可执行文件
-
名字查找
-
确定候选函数
-
寻找最佳匹配
77. 讲讲内联函数的理解?为什么不把所有的函数写成内联函数?
- 它以省去函数调用的开销来提高执行效率,但是果内联函数体内代码执行时间远大于函数调用开销,则没有太大的意义;另一方面每一处内联函数的调用都要复制代码,消耗更多的内存空间,因此以下情况不宜使用内联函数:
- 函数体内的代码比较长,将导致内存消耗代价
- 函数体内有循环,函数执行时间要比函数调用开销大
内联函数应该被视作简易工具函数,对于一些重复利用率高,代码数量较少的功能(例如比大小,交换位置),可以采用内联提高效率。
78. test.c 程序的编译过程
-
预处理就是进行宏替换、文件引入、取出空行和注释等。生成test.i文件
-
编译:词法分析,语法分析,最后进行代码优化生产汇编语言 。test.s文件
-
汇编:编译阶段的文件转换成目标文件,也就是机器代码(0,1)test.o文件
-
链接:将多个目标文件和库文件链接生成可执行文件的过程 a.out
-
一步执行指令:gcc test.c -o test
补充一个完整版本
(1)预编译 主要处理源代码文件中的以“#”开头的预编译指令。处理规则见下:
删除所有的#define,展开所有的宏定义。
处理所有的条件预编译指令,如“#if”、“#endif”、“#ifdef”、“#elif”和“#else”。
处理“#include”预编译指令,将文件内容替换到它的位置,这个过程是递归进行的,文件中包含其他 文件。
删除所有的注释,“//”和“/**/”。
保留所有的#pragma 编译器指令,编译器需要用到他们,如:#pragma once 是为了防止有文件被重 复引用。
添加行号和文件标识,便于编译时编译器产生调试用的行号信息,和编译时产生编译错误或警告是 能够显示行号。
(2)编译 把预编译之后生成的xxx.i或xxx.ii文件,进行一系列词法分析、语法分析、语义分析及优化后,生成相应 的汇编代码文件。
词法分析:利用类似于“有限状态机”的算法,将源代码程序输入到扫描机中,将其中的字符序列分 割成一系列的记号。
语法分析:语法分析器对由扫描器产生的记号,进行语法分析,产生语法树。由语法分析器输出的 语法树是一种以表达式为节点的树。
语义分析:语法分析器只是完成了对表达式语法层面的分析,语义分析器则对表达式是否有意义进
行判断,其分析的语义是静态语义——在编译期能分期的语义,相对应的动态语义是在运行期才能确定 的语义。 优化:源代码级别的一个优化过程。
目标代码生成:由代码生成器将中间代码转换成目标机器代码,生成一系列的代码序列——汇编语言 表示。
目标代码优化:目标代码优化器对上述的目标机器代码进行优化:寻找合适的寻址方式、使用位移 来替代乘法运算、删除多余的指令等。 (3)汇编
将汇编代码转变成机器可以执行的指令(机器码文件)。 汇编器的汇编过程相对于编译器来说更简单,没
有复杂的语法,也没有语义,更不需要做指令优化,只是根据汇编指令和机器指令的对照表一一翻译过
来,汇编过程有汇编器as完成。经汇编之后,产生目标文件(与可执行文件格式几乎一样)xxx.o(Windows
下)、xxx.obj(Linux下)。(4)链接 将不同的源文件产生的目标文件进行链接,从而形成一个可以执行的程序。链接分为静态链接和动态链 接:
静态链接 函数和数据被编译进一个二进制文件。在使用静态库的情况下,在编译链接可执行文件时,链接器从库
中复制这些函数和数据并把它们和应用程序的其它模块组合起来创建最终的可执行文件。空间浪费:因为每个可执行程序中对所有需要的目标文件都要有一份副本,所以如果多个程序对同一个
目标文件都有依赖,会出现同一个目标文件都在内存存在多个副本;更新困难:每当库函数的代码修改了,这个时候就需要重新进行编译链接形成可执行程序。
运行速度快:但是静态链接的优点就是,在可执行程序中已经具备了所有执行程序所需要的任何东西, 在执行的时候运行速度快。
动态链接 动态链接的基本思想是把程序按照模块拆分成各个相对独立部分,在程序运行时才将它们链接在一起形
成一个完整的程序,而不是像静态链接一样把所有程序模块都链接成一个单独的可执行文件。共享库:就是即使需要每个程序都依赖同一个库,但是该库不会像静态链接那样在内存中存在多分,副 本,而是这多个程序在执行时共享同一份副本;
更新方便:更新时只需要替换原来的目标文件,而无需将所有的程序再重新链接一遍。当程序下一次运
行时,新版本的目标文件会被自动加载到内存并且链接起来,程序就完成了升级的目标。性能损耗:因为把链接推迟到了程序运行时,所以每次执行程序都需要进行链接,所以性能会有一定损 失。
79 动态链接和静态链接
-
静态编译lib
- 所谓静态链接就是把函数或过程直接链接到可执行文件中,成为可执行程序中的一部分,当多个程序调用同样的函数时,内存里就会有这个函数的多个拷贝,浪费内存资源。
-
动态链接dll
- 动态链接则是
提供了一个函数的描述信息给可执行文件
(并没有内存拷贝),当程序被夹在到内存里开始运行的时候,系统会在底层创建DLL和应用程序之间的连接关系,当执行期间需要调用DLL函数时,系统才会真正根据链接的定位信息去执行DLL中的函数代码
- 动态链接则是
-
动态链接的优缺点是:优点一方面是缩小了执行文件本身的体积,另一方面是加快了编译速度,节省了系统资源。缺点是哪怕是很简单的程序,只用到了链接库的一两条命令,也需要附带一个相对庞大的链接库;二是如果其他计算机上没有安装对应的运行库,则用动态编译的可执行文件就不能运行。
80. 友元函数和友元类的理解
- 友元提供不同类之间,类和一般函数之间数据共享。具体的说就是为了使其他类的成员函数直接访问该类的私有变量。即:允许外面的类或函数去访问类的私有变量和保护变量,从而使两个类共享同一函数。
- 这样的好处和坏处是:友元的正确使用能提高程序的运行效率,但同时也破坏了类的封装性和数据的隐藏性,导致程序可维护性变差。
- 1)友元函数
- 有元函数是定义在类外的普通函数,不属于任何类,可以访问其他类的私有成员。但是需要在类的定义中声明所有可以访问它的友元函数。一个函数可以是多个类的友元函数,但是每个类中都要声明这个函数。
#include <iostream>
using namespace std;
class A
{
public:
friend void set_show(int x, A &a); //该函数是友元函数的声明
private:
int data;
};
void set_show(int x, A &a) //友元函数定义,为了访问类A中的成员
{
a.data = x;
cout << a.data << endl;
}
int main(void)
{
class A a;
set_show(1, a);
return 0;
}
- 2)友元类
- 友元类的所有成员函数都是另一个类的友元函数,都可以访问另一个类中的隐藏信息(包括私有成员和保护成员)。但是另一个类里面也要相应的进行声明
#include <iostream>
using namespace std;
class A
{
public:
friend class C; //这是友元类的声明
private:
int data;
};
class C //友元类定义,为了访问类A中的成员
{
public:
void set_show(int x, A &a) { a.data = x; cout<<a.data<<endl;}
};
int main(void)
{
class A a;
class C c;
c.set_show(1, a);
return 0;
}
补充几个注意点
-
友元关系
不能被继承
。 -
友元关系是
单向的
,不具有交换性。若类B是类A的友元,类A不一定是类B的友元,要看在类中是否有相应的声明。 -
友元关系
不具有传递性
。若类B是类A的友元,类C是B的友元,类C不一定是类A的友元,同样要看类中是否有相应的申明 -
类中通过使用关键字friend 来修饰友元函数,
但该函数并不是类的成员函数
,其声明可以放在类的私有部分,也可放在共有部分。友元函数的定义在类体外实现,不需要加类限定
。 -
友元函数可以访问类中的私有成员和其他数据,但是访问不可直接使用数据成员,需要通过对对象进行引用
。 -
友元函数在调用上同一般函数一样
,不必通过对对象进行引用。
#include <iostream>
#include <cstring>
using namespace std;
class persion{
public:
persion(char *pn);
//友元函数;
friend void setweigth(persion &p,int h);//注意,参数列表中一般会有一个引用类型的形参,原因参考上面的使用要点3和4;
void disp(); //类成员函数
private:
char name[20];
int weigth,age;
};
persion::persion(char *pn) //构造函数
{
strcpy(name,pn);
weigth=0;
}
void persion::disp()
{
cout<<name<<"--"<<weigth<<endl;
}
//友元函数的具体实现:这里没有类限定例如 (perion::setweigth)这种形式,这里可以与上面的disp()做个对比,一个属于类的成员,有限定,不属于类的成员函数,没有加限定。
void setweigth(persion &pn,int w)
{
strcpy(pn.name,pn);//实现字符串复制
pn.weigth=w; //私有成员数据赋值
}
void main()
{
persion p("zhansan");
//调用实现setweigth(),与一般函数调用一致。
setweigth(p,60);
p.disp(); //调用类的成员函数。
}
80. 回调函数其他作用
-
当发生某种事件时,系统或其他函数将会自动调用你定义的一段函数;
-
回调函数就相当于一个中断处理函数
,由系统在符合你设定的条件时自动调用
。为此,你需要做三件事:1,声明;2,定义;3,设置触发条件(中间函数),就是在你的函数中把你的回调函数名称转化为地址作为一个参数,以便于系统调用
; -
回调函数就是一个通过函数指针调用的函数。如果你把函数的指针(地址)作为参数传递给另一个函数
,当这个指针被用为调用它所指向的函数时,我们就说这是回调函数; -
因为可以把调用者与被调用者分开。调用者不关心谁是被调用者,所有它需知道的,只是存在一个具有某种特定原型、某些限制条件(如返回值为int)的被调用函数。
你到一个商店买东西,刚好你要的东西没有货,于是你在店员那里留下了你的电话,过了几天店里有货了,店员就打了你的电话,然后你接到电话后就到店里去取了货。
在这个例子里,你的电话号码就叫回调函数,你把电话留给店员就叫登记回调函数,店里后来有货了叫做 触发回调事件,店员给你打电话叫做 调用回调函数,你到店里去取货叫做 响应回调事件。
因为可以把调用者和被调用者分开,所以调用者不关心谁是被调用者。它只需知道存在一个具有特定原型和限制条件的被调用函数。简而言之,回调函数就是允许用户把需要调用的方法的指针作为参数传递给一个函数,以便该函数在处理相似事件的时候可以灵活的使用不同的方法。
81. static_cast比C语言中的转换强在哪里?
- const属性用const_cast。
- 基本类型转换用static_cast。
- 多态类之间的类型转换用daynamic_cast。
- 不同类型的指针类型转换用reinterpreter_cast。
-
更加安全;
-
更直接明显,能够一眼看出是什么类型转换为什么类型,容易找出程序中的错误;可清楚地辨别代码中每个显式的强制转;可读性更好,能体现程序员的意图
82. 如何阻止一个类被实例化?有哪些方法?
- 方法一:定义为抽象类
- 方法二:构造函数私用
貌似就用过这两种
83. memcpy,strncpy
void * memcpy ( void * destination, const void * source, size_t num );
- 将source指向的地址处的 num 个字节 拷贝到 destination 指向的地址处。注意,是字节。注意有返回值 返回拷贝后的首地址
char *strncpy(char *destinin, char *source, int maxlen) ;
- destinin:表示复制的目标字符数组;source:表示复制的源字符数组;maxlen:表示复制的字符串长度
84. sprintf
-
int sprintf( char *buffer, const char *format [, argument,...] );
-
sprintf函数的功能与printf函数的功能基本一样,只是它把结果输出到指定的字符串中了
-
sprintf(s,"%s%d%c",“test”,1,‘2’);
85. 你知道const char* 与string之间的关系是什么
- 之前有专门写过一篇各种转换
a) string转const char*
string s = “abc”;
const char* c_s = s.c_str();
b) const char* 转string,直接赋值即可
const char* c_s = “abc”;
string s(c_s);
c) string 转char*
string s = “abc”;
char* c;
const int len = s.length();
c = new char[len+1];
strcpy(c,s.c_str());
d) char* 转string
char* c = “abc”;
string s(c);
e) const char* 转char*
const char* cpc = “abc”;
char* pc = new char[strlen(cpc)+1];
strcpy(pc,cpc);
f) char* 转const char*,直接赋值即可
char* pc = “abc”;
const char* cpc = pc;
86. 类的初始化方式及其区别、效率
-
赋值初始化
,通过在函数体内进行赋值初始化;列表初始化
,在冒号后使用初始化列表进行初始化。 -
这两种方式的主要区别在于:
-
对于在函数体中初始化,
是在所有的数据成员被分配内存空间后才进行的
。 -
列表初始化是给数
据成员分配内存空间时就进行初始化
,就是说分配一个数据成员只要冒号后有此数据成员的赋值表达式(此表达式必须是括号赋值表达式),那么分配了内存空间后在进入函数体之前给数据成员赋值,就是说初始化这个数据成员此时函数体还未执行。
-
-
方法一是在构造函数当中做赋值的操作,而方法二是做纯粹的初始化操作。我们都知道,C++的赋值操作是会产生临时对象的。临时对象的出现会降低程序的效率。
class Animal
{
public:
Animal(int weight,int height): //A初始化列表
m_weight(weight),
m_height(height)
{
}
Animal(int weight,int height) //B函数体内初始化
{
m_weight = weight;
m_height = height;
}
private:
int m_weight;
int m_height;
};
87. 哪些情况必须使用成员列表初始化
① 当初始化一个引用成员时;
② 当初始化一个常量成员时;
③ 当调用一个基类的构造函数,而它拥有一组参数时;(或者说子类初始化父类的成员)
④ 当调用一个成员类的构造函数,而它拥有一组参数时;(或者说当数据成员是对象的时候)
补充
大端对齐和小端对齐
- Big-Endian就是高位字节排放在内存的低地址端,低位字节排放在内存的高地址端。(想象左旋)
- Little-Endian就是低位字节排放在内存的低地址端,高位字节排放在内存的高地址端。(右旋)
- 通过如下代码判断 比较好理解 16进制 需要4位 char 一个字节 8位 相当于代表2个数字 强制转换取低地址的两个数字 进行判断取到的值是12 还是 34
BOOL IsBigEndian()
{
int a = 0x1234;
char b = *(char *)&a; //通过将int强制类型转换成char单字节,通过判断起始存储位置。即等于 取b等于a的低地址部分
if( b == 0x12)
{
return TRUE;
}
return FALSE;