C++关键字和库函数

库函数

strcpy函数的缺陷

在于不检查目的缓冲区的大小边界,直接全部赋值。会产生覆盖其他变量的问题的。

1
2
3
4
5
6
7
char * strcpy(char * strDest,const char * strSrc) {
    if ((NULL==strDest) || (NULL==strSrc)) 
    throw "Invalid argument(s)"; 
    char * strDestCopy = strDest; 
    while ((*strDest++=*strSrc++)!='\0'); 
    return strDestCopy;
}

一般会用memcpy_s

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <cstdio>
#include <cstring>
int main() {
    char src[] = "Hello, world!";
    char dest[10];
    // 使用 memcpy_s
    errno_t result = memcpy_s(dest, sizeof(dest), src, sizeof(src) - 1);
    if (result != 0) {
        std::printf("Memory copy failed with error code %d\n", result);
    } else {
        std::printf("Memory copy succeeded: %s\n", dest);
    }
    return 0;
}

memmove的底层原理

对于memmove最简单的实现就是这样

1
2
3
4
5
6
psrc = (char *)src;
pdst = (char *)dst;
while (size--)
{
  *pdst++ = *psrc++;
}

但要是遇到这种情况:
地址位:[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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
void *memmove(void *dst, const void *src, size_t size)
{
    char *psrc;
    char *pdst;
    if (NULL == dst || NULL == src)
    {
        return NULL;
    }
    if ((src < dst) && (char *)src + size > (char *)dst) // 出现地址重叠的情况,自后向前拷贝
    {
        psrc = (char *)src + size - 1;
        pdst = (char *)dst + size - 1;
        while (size--)
        {
            *pdst-- = *psrc--;
        }
    }
    else
    {
        psrc = (char *)src;
        pdst = (char *)dst;
        while (size--)
        {
            *pdst++ = *psrc++;
        }
    }
    return dst;
}

关键字

函数相关的关键字

explicit的作用:

也就是说,为防止隐式类型转化而来的。在构造函数的外层进行修饰。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <iostream>
#include <cstring>
using namespace std;
class A
{
public:
    int var;
    explicit A(int tmp)
    {
        var = tmp;
        cout << var << endl;
    }
};
int main()
{
    A ex(100);
    A ex1 = 10; // error: conversion from 'int' to non-scalar type 'A' requested
    return 0;
}

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <iostream>
class Test {
private:
    int value;
public:
    Test() {
        printf("[Test] Constructor\n");
    }
    void* operator new(size_t size) {
        printf("[Test] operator new\n");
        return NULL;
    }
};
int main() 

    Test* t = new Test();
    return 0;
}

也就是先调用 new 操作符,然后调用类的构造函数,并且返回指针。

第二种生成对象数组,并且只能用delet来释放

1
2
int *arr = new int[100]
delete []arr;

第三种,对指定的地址new对象,释放的时候应该调用析构函数。

1
2
char buff[100];
int *p = new (buff) int(101);

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
2
3
4
int ra=0;
ra=0x1111;
ra=0x2222;
ra=0x3333; (正常情况下前两个赋值操作会给优化掉)

如果要检查,可以使用,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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <stdio.h>
typedef char* ptr;
#define PTR char*
int main()
{
ptr a, b, c;
PTR x, y, z;
printf("sizeof a:%zu\n" ,sizeof(a) );
printf("sizeof b:%zu\n" ,sizeof(b) );
printf("sizeof c:%zu\n" ,sizeof(c) );
printf("sizeof x:%zu\n" ,sizeof(x) );
printf("sizeof y:%zu\n" ,sizeof(y) );
printf("sizeof z:%zu\n" ,sizeof(z) );
return 0;
}

其实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
2
3
4
5
6
7
std::vector<int> vect; 
for(auto it = vect.begin(); it != vect.end(); ++it)
{  //it的类型是std::vector<int>::iterator
    std::cin >> *it;
}

auto ptr = [](double x){return x*x;};//类型为std::function<double(double)>函数对象

但是它有一些小细节:

  • 去除&, 和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 使用更多的内存,因为哈希表需要维护桶和链表等额外结构。