一、指针函数和函数指针
1 定义
指针函数本质是一个函数,其返回值为指针。
函数指针本质是一个指针,其指向一个函数。
2 写法
指针函数: int* fun(int x, int y);
函数指针: int (*fun)(int x, int y);
意义解读:
- int (*fun)(int x, int y); *与func结合,因此func是一个指针,指向返回值为int型且输入参数为(int x, int y)的函数的指针,称为函数指针。
- int* fun(int x, int y); *与int结合, 即int*是一个返回类型, fun是一个函数名,这个函数的返回类型为int*,称为指针函数。
二、常量指针和指针常量
1 定义
常量指针:const int* p
常量被一个指针指了,指针可以指向其它地址,但指向的内容是不可变的,是一个常量。
指针常量:int* const p
指针是一个常量,指针自身的值是一个常量,不可改变,始终指向同一个地址,在定义的同时必须初始化,不可以指向其他地方
int num1 = 10;
int num2 = 20;
int* const zzcl = &num1; //zzcl是指针常量,指向num1,zzcl指向的位置不能更改,但是指向位置存的值可以更改
const int* clzz = &num2; //clzz是常量指针,指向num2,clzz指向的位置可以更改,但是指向之后,该地址的值不能通过该常量指针去修改。
clzz = &num1; //让常量指针指向别的地方
cout << "常量指针指向所存的值为:"<<*clzz << endl;
*zzcl = num2; //指针常量指向的地方可以修改,但是这个地址也被常量指针指了,可以通过指针常量修改该地址的值,但不能通过常量指针修改
cout << "指针常量指向所存的值为:"<<*zzcl << " 常量指针指向所存的值为:" << *clzz;
三、define和内联函数inline
1 内联函数inline
在函数名前面添加关键字inline,该函数被声明为内联函数,当程序中出现对该函数的调用时,C++编译器会将函数体中的代码插入到调用该函数的代码出,使用实参代替形参,使程序运行时不再进行函数调用。
主要目的是为了消除调用函数时的系统开销,以提高代码的运行速度。
说明:
- 内联函数在第一次被调用前必须进行完整定义
- 内联函数体内不能含有复杂的控制语句,for switch等
- 内联函数是一中时间换空间的措施
- 内联函数代替宏定义,可以消除宏定义的不安全性(宏定义不检查函数参数,返回值)
2 宏定义#define
用来将一个标识符定义为一个字符串,被定义的字符串为文本替换。
宏展开是在预处理阶段完成的,这个阶段吧替换文本看成是一个字符串,不会做任何计算
有参宏定义
#define AREA(i) i*i
需要在宏体的每个参数加上括号,并在整个宏体上再加一个括号
#define AREA(i) i*i
#define AREA(i) (i)*(i)
#define AREA(i) ((i)*(i))
三种不同的定义方式,对于我们通常理解的计算方式来说,最后一种是最好的,时刻记住宏展开是文本替换,不会进行计算。
多行宏定义参数
#define AREA(num,Sum)\
if(r>=0)\
{\
Sum=num+num*2;\
}
int main()
{
int Sum;
int num = 2;
AREA(r,Sum);
}
常见预定义宏
_DATA_
代表日期_FILE_
代表当前源文件名_TIME_
代表时间_FUNCTION_
代表当前函数名_LINE_
代表当前程序行号
直接调用即可
int main()
{
cout << __DATE__ << endl;
cout << __FILE__ << endl;
cout << __TIME__ << endl;
cout << __FUNCTION__ << endl;
cout << __LINE__ << endl;
}
输出:
Aug 27 2023
c:\users\name\source\repos\project3\project3\源.cpp
12:14:37
main
17
3 内联函数与宏定义
他们都可以用来实现代码的快速执行,但是有区别:
- 参数类型安全性:内联函数比宏更安全,会对参数进行类型检查,但宏定义不会。
- 编译器优化:内联函数是在编译期间展开的,可以进行更多的编译器优化。宏定义在预处理器展开,不能进行编译器优化,内联函数通常有更好性能。
- 调试:内联函数更容易进行调试。内联函数是实际函数的一份副本,可以通过调试器跟踪到内联函数的执行过程,宏定义无法进行调试。
- 名称空间:内联函数位于名称空间之中,宏定义不属于任何名称空间,内联函数可以避免名称冲突,但是宏定义可能导致名称冲突
- 内联函数的调用是传参,宏定义只是进行简单的文本替换。
四、i++与++i
- i++ 先自增1,然后返回自增前的值
- ++i 先自增1,返回自增后的值
int main()
{
int i = 1;
cout << i++ <<' '<< i <<endl;
int j = 1;
cout << ++j << ' ' << j;
}
程序运行结果:
1 2
2 2
五、C中的static作用
- 隐藏。同时编译多个文件时,未加static前缀的全局变量和函数都具有全局可见性,使用static在不同文件中定义同名函数和同名变量不会出现命名冲突。
- 保持变量内容的持久。static修饰的变量存储在静态存储区,会在程序刚开始运行时完成初始化,唯一的一次初始化。 有两种变量存储在静态存储区:static变量和全局变量。它的值不会因为函数调用的结束而清除,函数再次调用时,它的值是上一次调用结束后的值。
- 默认初始化为0。
六、C与C++定义常量
C使用宏定义常量,C++使用const定义常量。
区别:
- const是有数据类型的常量,而宏定义没有,编译器可以对const变量进行及静态类型的安全检查,后者是字符替换。
- 有些编译器可以对const常量调试,不能对宏调试。
但是宏有不可替代的卫哨作用,防止文件的重复包含。
七、指针和引用
引用:起别名
int a = 10;
int &q = a;
底层里a和q是同一个东西,引用的底层实现是指针,等同于:
int *const q = &a;
指针和引用的区别:
引用是对象的别名,操作引用就是操作这个对象,必须在创建的同时有效地初始化(引用一个有效的对象,不可为NULL),引用具有指针的效率。
七、C与C++的内存分配方式
- 从静态存储区分配。内存在程序编译的时候就分配好了,这块内存在程序的整个运行期间都在,全局变量和static
- 在栈上创建。执行函数时,函数内局部变量的存储单元在栈上创建,函数执行结束时存储单元被自动释放。
栈区是编译器自动分配释放的,向低地址扩展的数据结构,是连续的内存区域。 - 在堆上分配,程序在运行的时候使用malloc或new申请任意多少的内存,动态内存。
堆区是由程序员分配释放,与数据结构中的堆是两回事,向高地址扩展,是不连续的内存区域。 - 文字常量区:存放的是程序中需要用到的常量,int型、字符串等,程序结束后由系统释放。
- 程序代码区:存放程序的二进制代码
静态内存和动态内存
- 静态内存分配是在程序编译时完成,不占用CPU资源;动态内存是程序运行时完成,占用CPU资源
- 静态内存在栈上分配;动态内存在堆上分配
- 静态内存控制权交给编译器;动态内存控制权交给程序员
1 //main.cpp
2 int a=0; //全局初始化区
3 char *p1; //全局未初始化区
4 main()
5 {
6 int b; //栈
7 char s[]="abc"; //栈
8 char *p2; //栈
9 char *p3="123456"; //123456\0在常量区,p3在栈上。
10 static int c=0; //全局(静态)初始化区
11 p1 = (char*)malloc(10);
12 p2 = (char*)malloc(20); //分配得来得10和20字节的区域就在堆区。
13 strcpy(p1,"123456"); //123456\0放在常量区,编译器可能会将它与p3所向"123456"优化成一个地方。
14 }
八、new/delete 和 malloc/free的区别
- malloc()与free()是C语言的标准库函数,new/delete是C++的运算符。
- malloc/free不允许重载,new/delete允许重载
- malloc()和free()不在编译器控制权限之内,不能把构造函数和析构函数的任务强加给他们
总的来说,new/delete能够对对象进行动态内存分配,但是malloc/free不行:
new一个对象时,会在编译器运行相应的构造函数以构造对象,并为其传入初值
delete一个对象时,会调用对象的析构函数完成对象的析构。malloc/free是不会的。 - new/delete返回完整类型指针,mallor/free返回void*类型指针
九、#include<a.h>与#include"a.h"的区别
- #include<a.h> 从标准库路径开始搜索
- #include"a.h" 从用户的工作路径开始搜索
十、extern “C” 的作用
在C++程序中调用被 C编译器编译后的函数加的语句。函数被C++编译后在库中的名字与C语言不同,C++提供了C连接交换指定符号extern “C” 来解决名字匹配问题
十一、C++和C的区别
- C++是面向对象的语言,C是面向过程的语言
- new/delete和malloc()/free()
- C++有引用的概念,C没有
- C++有类的概念,C没有
- C++引入函数重载的特性,C没有
C++的基本特征:
- 数据抽象
- 继承
- 多态
静态特性:程序的功能在编译的时候就确定下来的。
动态特性:程序的功能是在运行时刻才确定下来的称为动态特性。C++的虚函数,抽象基类,动态绑定和多态组成了出色的动态特性
十二、深浅拷贝
- 浅拷贝:将对象或原数组的引用直接赋值给新对象,新数组,他们只是对原对象的一个引用。
- 深拷贝:创建一个心得对象和数组,将原对象的各项属性的值拷贝过来,会在堆内存中额外申请空间来存储数据,当数据成员中有指针时,必须用深拷贝。
当用浅拷贝时,新对象的指针与原对象的指针指向了堆上的同一块儿内存,新对象和原对象析构时,新对象先把其指向的动态分配的内存释放了一次,而后原对象析构时又将这块已经释放过的内存再释放一次。对同一块动态内存执行2次以上释放的结果是未定义的,所有会导致内存泄漏或程序崩溃。
十三、set容器
C++的set是基于红黑树实现的,红黑树是一种自平衡二叉搜索树。在set中,每个元素都是唯一的,这是通过红黑树的性质来实现的。
红黑树的每个节点都有一个颜色属性,可以是红色或黑色。红黑树满足以下性质:
- 每个节点要么是红色,要么是黑色。
- 根节点是黑色。
- 每个叶子节点(NIL节点,空节点)是黑色的。
- 如果一个节点是红色的,则它的两个子节点都是黑色的。
- 对于每个节点,从该节点到其所有后代叶子节点的简单路径上,均包含相同数目的黑色节点。
在set中,每个元素都是红黑树的一个节点,插入元素时,先按照二叉搜索树的规则找到插入位置,然后将该节点插入红黑树中。如果插入的元素已经存在于set中,则不会插入,因为红黑树中不允许有重复元素。
通过红黑树的性质,保证了set中的元素唯一性。
十四、c++ list和vector
list
- 允许在序列中的任何位置执行固定O(1)时间复杂度的插入和删除,两个方向都可
- 使用双链表实现,每个节点有next,prev
- forward_list单链表实现,他们的主要缺点是不能通过位置直接访问元素,需要迭代访问,线性时间开销
- 节点不连续,已造成内存碎片,存储密度低
底层数据结构原理集合
- vector的底层数据结构为数组,支持快速随机访问。动态内存分配是通过每次新增元素,重新初始化一片内存将数组移动过去,最后要析构原有的vector并释放原有的内存。
- list底层数据结构为双向链表
- deque底层数据结构为一个中央控制器和多个缓冲区,支持首位快速增删。
- stack底层一般用list或deque实现,封闭头部即可。不用vector原因是扩容耗时
- queue底层用list或deque实现
- priority_queue的底层数据结构一般以vector为底层容器,堆为处理规则来管理底层容器
- set底层数据结构为红黑树,有序,不重复。不重复的原理是什么?
- multiset底层数据结构为红黑树,有序可重复
- map底层数据结构为红黑树,有序不可重复
- multimap底层数据结构为红黑树,有序可重复
- hash_set底层数据结构为hash表,无序不重复
十五、map底层
map底层用红黑树
红黑树为了弥补二分查找树多次单边插入导致不平衡而引入的解决办法。
为什么要用红黑树?AVL树呢?
平衡性能方面的考虑。
avl树高度平衡,红黑树通过增加节点颜色实现部分平衡,插入删除节点方面红黑树效率更高更稳定,查找效率比avl树低。
总结:实际应用中,若搜索的次数远远大于插入和删除,那么选择AVL,如果搜索,插入删除次数几乎差不多,应该选择RB。
十六、析构函数执行顺序
构造函数的执行顺序
- 基类构造函数
- 成员类构造函数
- 派生类构造函数
调用顺序与派生类构造函数冒号后面给出的初始化列表(Derived(): m1(), m2(), Base1(), Base2())没有任何关系,按照继承的顺序和变量再类里面定义的顺序进行初始化。 先继承Base2,就先构造Base2。先定义m2,就先构造m2。
析构函数执行顺序
- 调用派生类的析构函数
- 调用成员类对象析构函数
- 调用基类析构函数
全局变量、静态变量和局部变量
全局变量在程序开始时调用构造函数、在程序结束时调用析构函数。
静态变量在所在函数第一次被调用时调用构造函数、在程序结束时调用析构函数,只调用一次。
局部变量在所在的代码段被执行时调用构造函数,在离开其所在作用域(大括号括起来的区域)时调用析构函数。可以调用任意多次。
十七、虚函数怎么实现
怎么通过写虚函数实现多态:
概括一下就是基类的指针指向派生类对象时,通过基类的指针或引用调用虚函数,会执行实际所指派生类的虚函数实现
class A
{
public:
int a;
int b;
virtual void f() {}
};
class B : public A
{
public:
int x;
int y;
void f() override {}
};
int main()
{
B b;
A *x = &b;
x->f();
return 0;
}
当基类指针x指向派生类b时,调用f成员的时候每调用的实际是派生类B的成员函数。
多态的意思也就是在虚函数使用时,基类指针有种形态。
虚函数的底层实现
虚函数的底层实现主要通过:虚表指针、虚函数表
虚表指针,即在基类定义一个虚函数时,编译阶段会在类内添加虚表指针成员,当x86时占用4字节、x64时占用8字节,该指针指向一个虚函数表的首地址,虚函数表是该类的各虚函数地址组成的数组,每个类使用一个虚函数表,每个类有一个虚表指针去指向对应的虚函数表。
类A对象进行如上定义时,在内存中的分布是这样的
其中的func1、func2是A的组成部分,但是不会占用类A的内存空间,类A对象所占用的内存空间是成员变量和编译时生成的虚函数表指针vptr,在x64平台,sizeof(a) = 4+4+8 = 16
虚函数实现多态的原理:概括讲即若子类重写了父类的虚函数,那么子类的虚函数表就会在对应的虚函数地址修改为自己重写的虚函数的地址,子类对象地址赋值给父类对象指针时,父类对象的虚函数表已经会变成子类函数的虚函数表,由此实现多态。
在对象继承情况时析构函数需要实现为虚函数,因为父类指针 new 为一个子类对象时,若不使用虚函数则调用的是父类对象的析构函数,需要用虚函数去进行子类对象的析构。