C++关键字和库函数
库函数
strcpy函数的缺陷
在于不检查目的缓冲区的大小边界,直接全部赋值。会产生覆盖其他变量的问题的。
1 | char * strcpy(char * strDest,const char * strSrc) { |
一般会用memcpy_s
1 | #include <cstdio> |
memmove的底层原理
对于memmove最简单的实现就是这样
1 | psrc = (char *)src; |
但要是遇到这种情况:
地址位:[123456789
src: abcd
dst:abcd (这种情况问题很大,地址堆叠,1号位置的a移动到2号位置取代了b,然后2号位置变成了a然后移动到3号位置)
dst: ******
src:****** (反之情况毫无问题,src移到dst)
遇到地址重叠的时候处理条件怎么写?
也就是这里有个问题在于,位置重叠,src<dst, (char*)src+size>char(*)dst 的话才算是地址重叠,但是src也可以大于dst的时候,发生位置重叠。这个时候自后向前move就不管用了,但是默认的自前向后就管用。
1 | void *memmove(void *dst, const void *src, size_t size) |
关键字
函数相关的关键字
explicit的作用:
也就是说,为防止隐式类型转化而来的。在构造函数的外层进行修饰。
1 | #include <iostream> |
lambda表达式的应用
公式:
[capture list](parameter list)->return type { function body }
捕获方式:
[空] 表示没有捕获任何变量
[x, &y] 表示x以值传递的方式传入,y以引用的方式传入
[&] 表示外部变量都引用传入
[=] 表示外部变量都值传入
[&, x] 表示除了x都引用传入, x值传递
[=, &x] 表示除了x都值传入,其他引用传入
值传入的不能修改,除非添加mutable修饰符
auto f = t mutable { t++; return t; }. ps毕竟是值传递而已,没有办法返回
悬挂引用问题:
有时候捕获的引用,会引起悬挂引用的问题,如果函数已经清理改局部变量,但是lambda表达式已经return出去了,请注意
一些用法:
c++14后lambda表达式允许范型,比如说:auto lambda = [](auto x, auto y) {return x+y}
也支持在捕获列表初始化:auto lambda = [value=1] {return y;}
c++17支持constexpr修饰符,可优化很多:auto answer = y constexpr { return y+10;}
当然最常用的是使用lambda表达式进行排序:
sort(arr, arr+4, [=](int x, int y)->bool{return x<y;})
inline函数的工作原理
内联函数编译器在函数调用处直接展开该函数,没有了调用普通函数的切换栈帧,压参等开销时间。并且依旧保留函数指针。
优点:在于不会产生,函数调用开销。而且还可以编译器还会针对上下文进行其他的优化,比如定量折叠。
缺点:如果使用过度,二进制文件变很大,函数消耗寄存器变多,增加编译开销。
一般适度使用,则给短小处理简单的函数内联
不能内联的:
( 有循环的,有静态变量,有递归的,返回类型不是void,不存在return语句,包含switch和goto,都不内联)
inline的作用和使用方法
inline函数如何高效有什么优缺点,已经熟透了。
但是如何使用?在头文件定义,然后被多个.cpp包含,这样就不会出现定义错误内联函数
但是虚函数没有办法内联的,毕竟虚函数是运行时决定的。
类成员函数都会自动内联不需要加inline。
类外成员函数则需要添加inline的关键字。
返回函数中静态变量的地址
由于静态局部变量也好,静态全局变量也好,都在静态区,存在于程序的整个生命周期内。
即使返回出去了,到了外部,静态局部变量的地址此时此刻被外部的变量映射了,所以依旧能够访问的。
宏定义和内联函数的区别
内联函数虽然是在编译时期展开,但是依旧对参数类型进行检查,和是否能正常编译进行检查,而且还保留了调试信息,而且类的成员函数都是内联函数,可以访问类成员,而且参数传递只计算一次。
宏定义,则是编译预处理时期进行文本替换,不会检查类型,也不会检查能否宏函数体能否正常编译,不保留调试信息,不可以访问类成员,而且每次使用宏都会计算表达式参数, 运行中可能计算多次。
extern C的作用
在某些场合,需要使用C来提高效率。在C++调用C的函数有问题。
因为C++的函数经过编译,和C函数经过编译得到的函数名是不一样的。C++因为支持函数重载,符号表里的函数名可能是_Z4function_name,而C经过编译函数名是function_name。
所以如果调用外部一个C函数,需要使用
extern “C” {
int strcmp(const char*, const char*);
}
用宏实现对比大小,以及两个数字的最值
#include
#define MAX(X,Y) ( (X)>(Y) ? (X) : (Y) )
#define MIN(X,Y) ( (X)<(Y)? (X): (Y))
int main(){
int var1 = 10, int var2 =20;
cout<<MAX(var1, var2)<<endl;
return 0;
}
点评:这样不好, 如果使用 MAX(var1++,var2), 则其实会变成
(var1++) > (var2) ? (var1++) : (var2) 则变成var1++执行了两次。
#define MAX(x, y) ({ \
typeof(x) _max1 = (x); \
typeof(y) _max2 = (y); \
(void) (&_max1 == &_max2); \
_max1 > _max2 ? _max1 : _max2; })
点评:这样的优化本质就是,让中间临时变量来承载,这样x只会出现一次。
变量关键字
sizeof(1==1)在c和c++不一样:
sizeof(X) 里面接受对象或者是表达式,但是不会对该表达式进行计算,只会进行类型推导,然后返回该类型占有得字节大小。 在c里面, sizeof(1==1) 等价于 sizeof( int ), 所以返回4, 在c++则会返回bool类型就是1。
new的作用
第一种也就是最常见的,生成对象本身。
1 | #include <iostream> |
也就是先调用 new 操作符,然后调用类的构造函数,并且返回指针。
第二种生成对象数组,并且只能用delet来释放
1 | int *arr = new int[100] |
第三种,对指定的地址new对象,释放的时候应该调用析构函数。
1 | char buff[100]; |
new和malloc的区别
申请内存:
new在申请内存的时候,回调用对象的构造函数,对象初始化。
malloc不会,malloc仅仅在堆上申请一块指定大小的内存空间。
符号与函数:
new是一个操作符,malloc是一个c的函数
返回类型:
new的返回值是一个对象的指针类型,malloc统一返回void*指针
用法:
new在堆上不仅申请内存且调用了对象构造函数初始化,malloc可以在堆上申请空间
失败:
new分配失败抛bad alloc异常,malloc分配失败会返回null指针
空间大小:
new空间由编译器计算(对齐),malloc需要指定空间大小
运算符号重载:
new可以运算符重载,malloc不支持运算符重载
申请更改:
new一旦申请不可以更改,malloc申请的可以通过realloc重新指定空间大小
volatile的作用和使用场景
编译器通常会对变量的读写做一系列的优化,会为了速度把变量缓存到寄存器不写回,优化掉一些汇编指令,本来是好事,但是一些场景反而耽误了事情。
比如:需要操作某些硬件,从而按特定指令读写寄存器,尤其在芯片手册里很多。还有多线程的情况下,由于编译优化,共享资源值其实没有写回去。
1 | int ra=0; |
如果要检查,可以使用,g++ -S -O3 test.cpp -o test,去查看汇编。
但是把volatile int ra=0; 那就不一样的了。
delete和free的区别
delete和free一样都是操作符号,可以对数组对象进行delete,每次操作都是调用对象的析构函数。而且可以重载。
free是C的一个函数,用来释放malloc和realloc申请的内存,只是将指针指向的内存还给操作系统。free了之后记得把指针置空,不然访问已经free掉的内存空间有问题。但是也有可能有double free的问题。
类型关键字
difine和typedef的区别
define:是c++里头最骚的东西,基本所有骚操作都是由它弄出的,而且又很难定位。
因为它在编译预处理的时候就是可以做替换操作。且#define在.cpp里面是全局的,在头文件里只要包含了就是可以使用。
typedef:是编译时处理,具有类型检查功能,给一个存在的类型一个别名。这是#define做不到的,它只是定义变量,常量,和编译开关,还有一些比如定义 __attribute(constructor)__的函数做预处理。typedef是在函数外定义则文件可用,函数里定义则只有函数里可用。
坏处:
而且define很容易翻车,比如
1 | #include <stdio.h> |
其实y,z是char类型,因为#define只是替换文本。
class和struct的异同
其实没有啥区别在c++里面
本质区别在于
- 1 class的成员默认是private,struct成员默认是public
- 2 class默认继承是private继承,struct是public继承
- 3 class可以用模版编程,struct不行
auto类型推导原理
auto的使用是必须的,因为auto可以推导lambda表达式的类型。
- 1 它主要是声明变量自动推导类型。
- 2 声明函数的返回值推导类型占用符。
使用也方便很多,请看下文
1 | std::vector<int> vect; |
但是它有一些小细节:
- 去除&, 和volatile。
- 遇到{} ,会得到 std::initializer_list
类型 - auto & 则是左值引用
- auto && 则是右值引用
define和const的区别
define在编译预处理阶段进行替换,const是在编译阶段确认值。
define只是代码替换没有类型检查等等,不太安全,const的常量还是有类型的。
define替换来替换去,也只是增加了代码段空间,const是在静态区的只读空间。
define无法调试,const可以。
define可以接受构造复杂的表达式,const不行。
struct和union的区别
union是只有一个有效成员,struct所有成员都有效。
union对于节省空间有奇效。因为其大小只是里面最大的变量值,且不能包含不确定的长度变量如Arr[]这样的。
struct的分配大小是根据对齐策略,但是union的策略则是根据最大的值类型的倍数分配大小,至于这个倍数是多少,则不知道!
C和C++的struct区别
在C里面struct是没有权限设置的,通常是一堆变量的结合,而且不能定义成员函数。而且在定义的时候需要, struct A var; 而且更没有C++的一系列花里胡哨的操作,比如继承和多态。
但是C++里面的struct有权限设置,且可以定义成员函数,声明的时候就是,A var; 而且还可以继承,多态等等。 本质上C++为了兼容才保留了struct关键字。
容器
unorder_map 和 map本身有什么区别,在find的时间复杂度上?
std::map 和 std::unordered_map 是 C++ 标准库中常用的关联容器,它们在底层实现和操作特性上有显著的区别,尤其是在查找操作的时间复杂度方面。
std::map
底层实现:std::map 是一个基于红黑树(Red-Black Tree)的有序关联容器。
键的顺序:元素按键值排序存储,默认按升序排列,可以自定义排序规则。
时间复杂度:
插入、删除、查找操作的平均时间复杂度为 O(log n),最坏情况下也是 O(log n)。
由于 std::map 是有序的,所以在执行这些操作时需要维护树的平衡,这导致了对数时间复杂度。这里n代表的是键值对数目,所以树的高度是log(n)级别的。
std::unordered_map
底层实现:std::unordered_map 是一个基于哈希表(Hash Table)的无序关联容器。
键的顺序:元素无特定顺序存储,按哈希值存储。
时间复杂度:
插入、删除、查找操作的平均时间复杂度为 O(1)。
最坏情况下(当所有键都被哈希到同一个桶里,形成一个链表),插入、删除、查找操作的时间复杂度会退化为 O(n),不过这种情况很少发生,如果选择好的哈希函数并且负载因子保持合理,平均情况下仍然是 O(1)。
具体比较
有序性:std::map 保持键的有序性,而 std::unordered_map 不保持键的有序性。如果你需要保持键的顺序,使用 std::map;否则,使用 std::unordered_map。
性能:对于查找操作,如果你需要常数时间复杂度(O(1)),并且不关心键的顺序,std::unordered_map 通常更快。对于大多数情况下,std::unordered_map 的查找比 std::map 的对数时间复杂度(O(log n))更高效。
内存开销:std::unordered_map 由于哈希表的实现,可能会比 std::map 使用更多的内存,因为哈希表需要维护桶和链表等额外结构。