c++面经整理(一) c++基础

1 C++ 中 sizeof 关键字

1.1 sizeof 关键字

sizeof计算大小的小技巧

· 指针的大小永远是固定的,取决于处理器位数,32位就是 4 字节,64位就是 8 字节

· 数组作为函数参数时会退化为指针,大小要按指针的计算。

· struct 结构体要考虑字节对齐。

· 字符串数组要算上末尾的 '\0'

// 前提: 64 位电脑上
char  str[] = "Hello World" ;
char   *p = str;
double *dp;
int n = 10;

sizeof(str )=___12_____  // 11个字符 + 末尾'\0',总结第四点
sizeof ( p ) =  ___8 ___  // 64 位电脑,指针 8 个字节
sizeof ( n ) = ___4______  // int 一般 4 个字节
  
void Func (char str[10])
{
   sizeof( str ) = _8__  // 数组做参数退化为 char类型指针,即 8 个字节,总结第2点
}
void *vp = malloc( 100 );

sizeof ( vp )=__8____  // vp 是一个 void 类型指针,还是 8 个字节

struct AlignedStruct {
    char a;   // 本来1字节,padding 3 字节
    int b;    //  4 字节
    short c;  // 本来 short 2字节,但是整体需要按照 4 字节对齐(成员对齐边界最大的是int 4) ,
    //所以需要padding 2,总共: 4 + 4 + 4
};
sizeof(AlignedStruct) = ___12__


struct MyStruct {
    int a;
    double b;
    char c;
};//24
//int a 位于 0~3 字节,占 4 个字节。
//double b 需要对齐到 8 字节边界,所以在 4~7 字节填充 4 个字节的 padding,并从字节 8 开始放置 //double b, 占据 8 至 15 字节。
//char c 位于 16 字节,占 1 个字节。
//由于 double 需要对齐到 8 字节边界,结构体的整体大小也需要遵循此规则,所以 MyStruct 的总大小在 //17 至 24 字节之间又增加了 7 个字节的 padding,使其总大小达到 24 字节。

int arr[10];//40

struct EmptyStruct {};//1

1.2 sizeof 和 strlen

strlen
strlen 是一个 C 标准库中的函数,用于计算 C 风格字符串(以空字符 '\0' 结尾的字符数组)的长度,即不包括结尾的空字符的字符个数。

#include <iostream>
#include <cstring>

int main() {
    char str[] = "Hello, world!";
    std::cout << "Length of str: " << strlen(str) << std::endl; // 输出字符串 str 的长度
}
strlen 源代码如下:

size_t strlen(const char *str) {
    size_t length = 0;
    while (*str++)
        ++length;
    return length;
}

#sizeof
sizeof 是一个 C++ 编译期间计算的操作符,用于计算数据类型或对象所占用的字节数

#include <iostream>

int main() {
    int a = 42;
    std::cout << "Size of int: " << sizeof(int) << std::endl;    // 输出 int 类型的大小
    std::cout << "Size of a: " << sizeof(a) << std::endl;        // 输出变量 a 的大小
    std::cout << "Size of double: " << sizeof(double) << std::endl; // 输出 double 类型的大小
}

2. 数据类型

2.1 整型

C++ 整型数据长度标准
short ⾄少 16 位;int ⾄少与 short ⼀样长;long ⾄少 32 位,且⾄少与 int ⼀样长;long long ⾄少 64 位,且⾄少与 long ⼀样长;在使⽤8位字节的系统中,1 byte = 8 bit;很多系统都使⽤最⼩长度,short 为 16 位即 2 个字节,long 为 32 位即 4 个字节,long long 为 64 位即 8 个字节,
int 的长度较为灵活,⼀般认为 int 的长度为 4 个字节,与 long 等长。

头⽂件climits定义了符号常量:例如:INT_MAX 表⽰ int 的最⼤值,INT_MIN 表⽰ int 的最⼩值。

2.2 ⽆符号类型

即为不存储负数值的整型,可以增⼤变量能够存储的最⼤值,数据长度不变。

3. 指针

3.1 C/C++中数组做参数退化为指针

C++ 面试中还有一个比较常见的考题,就是会将一个数组做参数,然后在函数内部用 sizeof 去判断这个数组参数的大小,如下:

int func(char array[]) {
    printf("sizeof=%d\n", sizeof(array));//8
    printf("strlen=%d\n", strlen(array));//11
}

int main() {
    char array[] = "Hello World";
    printf("sizeof=%d\n", sizeof(array));//12
    printf("strlen=%d\n", strlen(array));//11
    func(array);
}

3.2 指针和引用的区别

  • 指针是一个新的变量,指向另一个变量的地址,我们可以通过访问这个地址来修改另一个变量;而引用是一个别名,对引用的操作就是对变量的本身进行操作

  • 指针可以有多级,引用只有一级

  • 传参的时候,使用指针的话需要解引用才能对参数进行修改,而使用引用可以直接对参数进行修改

  • 指针的大小一般是4个字节,引用的大小取决于被引用对象的大小

  • 指针可以为空,引用不可以。

数组做参数退化为指针 

数组退化:在 C++ 中,数组在作为函数参数时会退化为指向其首元素的指针。

退化的原因是因为数组作为函数参数时,实际传递的是指向数组首元素的指针,不可能逐个拷贝整个数组然后在栈上传递,所以编译器只知道参数是一个指针,而不知道它的长度信息。

但是,当数组直接作为 sizeof 的参数时,它不会退化,因为 sizeof 是编译器在编译期间计算的结果,这个时候编译器是有信息知道数组的大小。

为了在函数中获取数组的长度,需要将数组的长度作为另一个参数传递给函数,或者使用模板实现。

4 C++ 中 const 关键字

4.1修饰变量

当 const 修饰变量时,该变量将被视为只读变量,即不能被修改。

对于确定不会被修改的变量,应该加上 const,这样可以保证变量的值不会被无意中修改,也可以使编译器在代码优化时更加智能。

const int a = 10;
a = 20; // 编译错误,a 是只读变量,不能被修改

在 C++ 中,将 const 类型的指针强制转换为非const 类型的指针被称为类型强制转换(Type Casting),这种行为称为 const_cast。

4.2修饰函数参数

当 const 修饰函数参数时,表示函数内部不会修改该参数的值。这样做可以使代码更加安全,避免在函数内部无意中修改传入的参数值。

尤其是 引用 作为参数时,如果确定不会修改引用,那么一定要使用 const 引用。

例如:

void func(const int a) {
    // 编译错误,不能修改 a 的值
    a = 10;
}

4.3 修饰指针或引用

在 C/C++ 中,const 关键字可以用来修饰指针,用于声明指针本身为只读变量或者指向只读变量的指针。

根据 const 关键字的位置和类型,可以将 const 指针分为以下三种情况:

指向只读变量的指针
这种情况下,const 关键字修饰的是指针所指向的变量,而不是指针本身。

因此,指针本身可以被修改(意思是指针可以指向新的变量),但是不能通过指针修改所指向的变量。

const int* p;  // 声明一个指向只读变量的指针,可以指向 int 类型的只读变量
int a = 10;
const int b = 20;
p = &a;  // 合法,指针可以指向普通变量
p = &b;  // 合法,指针可以指向只读变量
*p = 30;  // 非法,无法通过指针修改只读变量的值

在上面的例子中,我们使用 const int* 声明了一个指向只读变量的指针 p。

我们可以将指针指向普通变量或者只读变量,但是无法通过指针修改只读变量的值。

 只读指针
这种情况下,const 关键字修饰的是指针本身,使得指针本身成为只读变量。

因此,指针本身不能被修改(即指针一旦初始化就不能指向其它变量),但是可以通过指针修改所指向的变量。

int a = 10;
int b = 20;
int* const p = &a;  // 声明一个只读指针,指向 a
*p = 30;  // 合法,可以通过指针修改 a 的值
p = &b;  // 非法,无法修改只读指针的值

在上面的例子中,我们使用 int* const 声明了一个只读指针 p,指向变量 a。我们可以通过指针修改 a 的值,但是无法修改指针的值。

只读指针指向只读变量
这种情况下,const 关键字同时修饰了指针本身和指针所指向的变量,使得指针本身和所指向的变量都成为只读变量。

因此,指针本身不能被修改,也不能通过指针修改所指向的变量。

const int a = 10;
const int* const p = &a;  // 声明一个只读指针,指向只读变量 a
*p = 20;  // 非法,无法通过指针修改只读变量的值
p = nullptr;  // 非法,无法修改只读指针的值

常量引用
常量引用是指引用一个只读变量的引用,因此不能通过常量引用修改变量的值。

const int a = 10;
const int& b = a;  // 声明一个常量引用,引用常量 a
b = 20;  // 非法,无法通过常量引用修改常量 a 的值

修饰成员函数
当 const 修饰成员函数时,表示该函数不会修改对象的状态(就是不会修改成员变量)。

这样有个好处是,const 的对象就可以调用这些成员方法了,因为 const 对象不允许调用非 const 的成员方法。

也很好理解,既然对象是 const 的,那我怎么保证调用完这个成员方法,你不会修改我的对象成员变量呢?那就只能你自己把方法声明未 const 的呢~

例如:

class A {
public:
    int func() const {
        // 编译错误,不能修改成员变量的值
        m_value = 10;
        return m_value;
    }
private:
    int m_value;
};

这里还要注意,const 的成员函数不能调用非 const 的成员函数,原因在于 const 的成员函数保证了不修改对象状态,但是如果调用了非 const 成员函数,那么这个保证可能会被破坏。

总之,const 关键字的作用是为了保证变量的安全性和代码可读性。

我对const的理解就是const在什么变量的左边,变量就不能被修改。

5 C++ 中 static 关键字

5.1 static 修饰全局变量

static 修饰全局变量可以将变量的作用域限定在当前文件中,使得其他文件无法访问该变量。 同时,static 修饰的全局变量在程序启动时被初始化(可以简单理解为在执行 main 函数之前,会执行一个全局的初始化函数,在那里会执行全局变量的初始化),生命周期和程序一样长。

// a.cpp 文件
static int a = 10;  // static 修饰全局变量
int main() {
    a++;  // 合法,可以在当前文件中访问 a
    return 0;
}

// b.cpp 文件
extern int a;  // 声明 a
void foo() {
    a++;  // 非法,会报链接错误,其他文件无法访问 a
}

5.2static 修饰局部变量

static 修饰局部变量可以使得变量在函数调用结束后不会被销毁,而是一直存在于内存中,下次调用该函数时可以继续使用。

同时,由于 static 修饰的局部变量的作用域仅限于函数内部,所以其他函数无法访问该变量。

void foo() {
    static int count = 0;  // static 修饰局部变量
    count++;
    cout << count << endl;
}

int main() {
    foo();  // 输出 1
    foo();  // 输出 2
    foo();  // 输出 3
    return 0;
}

5.3 static 修饰函数

static 修饰函数可以将函数的作用域限定在当前文件中,使得其他文件无法访问该函数。

同时,由于 static 修饰的函数只能在当前文件中被调用,因此可以避免命名冲突和代码重复定义。

// a.cpp 文件
static void foo() {  // static 修饰函数
    cout << "Hello, world!" << endl;
}

int main() {
    foo();  // 合法,可以在当前文件中调用 foo 函数
    return 0;
}

// b.cpp 文件
extern void foo(); // 声明 foo
void bar() {
    foo();  // 非法,会报链接错误,找不到 foo 函数,其他文件无法调用 foo 函数
}

5.4static 修饰类成员变量和函数

static 修饰类成员变量和函数可以使得它们在所有类对象中共享,且不需要创建对象就可以直接访问。

class MyClass {
public:
    static int count;  // static 修饰类成员变量
    static void foo() {  // static 修饰类成员函数
        cout << count << endl;
    }
};
// 访问:

MyClass::count;
MyClass::foo();

6 字节对齐

在C/C++中,字节对齐是内存分配的一种策略。

当分配内存时,编译器会自动调整数据结构的内存布局,使得数据成员的起始地址与其自然对齐边界(一般为自己大小的倍数)相匹配。

 字节对齐有助于提高内存访问速度,因为许多处理器都优化了对齐数据的访问。但是,这可能会导致内存中的一些空间浪费。

6.1字节对齐规则
以下是字节对齐的一些基本规则:

#1. 自然对齐边界
对于基本数据类型,其自然对齐边界通常为其大小。

例如,char 类型的自然对齐边界为 1 字节,short 为 2 字节,int 和 float 为 4 字节,double 和 64 位指针为 8 字节。具体数值可能因编译器和平台而异。

#2. 结构体对齐
结构体内部的每个成员都根据其自然对齐边界进行对齐。

也就是可能在成员之间插入填充字节。

结构体本身的总大小也会根据其最大对齐边界的成员进行对齐(比如结构体成员包含的最长类型为int类型,那么整个结构体要按照4的倍数对齐),以便在数组中正确对齐。

#3. 联合体对齐
联合体的对齐边界取决于其最大对齐边界的成员。联合体的大小等于其最大大小的成员,因为联合体的所有成员共享相同的内存空间。

#4. 编译器指令
可以使用编译器指令(如 #pragma pack)更改默认的对齐规则。这个命令是全局生效的。这可以用于减小数据结构的大小,但可能会降低访问性能。

#5. 对齐属性
在 C++11 及更高版本中,可以使用 alignas 关键字为数据结构或变量指定对齐要求。这个命令是对某个类型或者对象生效的。例如,alignas(16) int x; 将确保 x 的地址是 16 的倍数。

#6. 动态内存分配
大多数内存分配函数(如 malloc 和 new)会自动分配足够对齐的内存,以满足任何数据类型的对齐要求。

#include <iostream>

#pragma pack(push, 1) // 设置字节对齐为 1 字节,取消自动对齐
struct UnalignedStruct {
    char a;
    int b;
    short c;
};
#pragma pack(pop) // 恢复默认的字节对齐设置

struct AlignedStruct {
    char a;   // 本来1字节,padding 3 字节
    int b;    //  4 字节
    short c;  // 本来 short 2字节,但是整体需要按照 4 字节对齐(成员对齐边界最大的是int 4) 
              // 所以需要padding 2
   // 总共: 4 + 4 + 4
};

struct MyStruct {
 double a;    // 8 个字节
 char b;      // 本来占一个字节,但是接下来的 int 需要起始地址为4的倍数
              //所以这里也会加3字节的padding
 int c;       // 4 个字节
 // 总共:  8 + 4 + 4 = 16
};

struct MyStruct1 {
 char b;    // 本来1个字节 + 7个字节padding
 double a;  // 8 个字节
 int c;     // 本来 4 个字节,但是整体要按 8 字节对齐,所以 4个字节padding
  // 总共: 8 + 8 + 8 = 24
};


int main() {
    std::cout << "Size of unaligned struct: " << sizeof(UnalignedStruct) << std::endl; 
    // 输出:7
    std::cout << "Size of aligned struct: " << sizeof(AlignedStruct) << std::endl; 
    // 输出:12,取决于编译器和平台
    std::cout << "Size of aligned struct: " << sizeof(MyStruct) << std::endl; 
    // 输出:16,取决于编译器和平台
    std::cout << "Size of aligned struct: " << sizeof(MyStruct1) << std::endl;
     // 输出:24,取决于编译器和平台
    return 0;
}

7 C/C++ 字节序

8 C++ 中 extern 作用

一般而言,C++全局变量的作用范围仅限于当前的文件,但同时C++也支持分离式编译,允许将程序分割为若干个文件被独立编译。

于是就需要在文件间共享变量数据,这里extern就发挥了作用。

extern 用于指示变量或函数的定义在另一个源文件中,并在当前源文件中声明。 说明该符号具有外部链接(external linkage)属性。

也就是告诉编译器: 这个符号在别处定义了,你先编译,到时候链接器会去别的地方找这个符号定义的地址。

声明变量或函数的存在,但不进行定义,让编译器在链接时在其他源文件中查找定义。

这使得不同的源文件可以共享相同的变量或函数。

当链接器在一个全局变量声明前看到 extern 关键字,它会尝试在其他文件中寻找这个变量的定义。

这里强调全局且非常量的原因是,全局非常量的变量默认是外部链接的。

//fileA.cpp
int i = 1;         //声明并定义全局变量i

//fileB.cpp
extern int i;    //声明i,链接全局变量

//fileC.cpp
extern int i = 2;        //错误,多重定义
int i;                    //错误,这是一个定义,导致多重定义
main()
{
    extern int i;        //正确
    int i = 5;            //正确,新的局部变量i;
}

9 堆和栈

9.1 堆和栈有什么区别

  • 从定义上:堆是由new和malloc开辟的一块内存,由程序员手动管理,栈是编译器自动管理的内存,存放函数的参数和局部变量。

  • 堆空间因为会有频繁的分配释放操作,会产生内存碎片

  • 堆的生长空间向上,地址越来越大,栈的生长空间向下,地址越来越小。

9.2 堆和栈谁快一点?

栈快一点。因为操作系统会在底层对栈提供支持,会分配专门的寄存器存放栈的地址,栈的入栈出栈操作也十分简单,并且有专门的指令执行,所以栈的效率比较高也比较快。

堆的操作是由C/C++函数库提供的,在分配堆内存的时候需要一定的算法寻找合适大小的内存。并且获取堆的内容需要两次访问,第一次访问指针,第二次根据指针保存的地址访问内存,因此堆比较慢

10 malloc/free/new/delete

10.1 new和delete是如何实现的,new 与 malloc的异同处

在new一个对象的时候,首先会调用malloc为对象分配内存空间,然后调用对象的构造函数。delete会调用对象的析构函数,然后调用free回收内存。

new与malloc都会分配空间,但是new还会调用对象的构造函数进行初始化,malloc需要给定空间大小,而new只需要对象名。

10.2 既然有了malloc/free,C++中为什么还需要new/delete呢?

  • malloc/free和new/delete都是用来申请内存和回收内存的。

  • 在对非基本数据类型的对象使用的时候,对象创建的时候还需要执行构造函数,销毁的时候要执行析构函数。而malloc/free是库函数,是已经编译的代码,所以不能把构造函数和析构函数的功能强加给malloc/free。

10.3 delete和delete[]的区别

  • delete只会调用一次析构函数,而delete[]会调用每个成员的析构函数

  • 用new分配的内存用delete释放,用new[]分配的内存用delete[]释放

11 C++强制类型转换

四种强制类型转换操作符分别为:static_cast、dynamic_cast、const_cast、reinterpret_cast

  • 1)static_cast : 用于各种隐式转换。具体的说,就是用户各种基本数据类型之间的转换,比如把int换成char,float换成int等。以及派生类(子类)的指针转换成基类(父类)指针的转换

    特性与要点:

    1. 它没有运行时类型检查,所以是有安全隐患的。

    2. 在派生类指针转换到基类指针时,是没有任何问题的,在基类指针转换到派生类指针的时候,会有安全问题。

    3. static_cast不能转换const,volatile等属性

  • 2)dynamic_cast: 用于动态类型转换。具体的说,就是在基类指针到派生类指针,或者派生类到基类指针的转换。 dynamic_cast能够提供运行时类型检查,只用于含有虚函数的类。 dynamic_cast如果不能转换返回NULL。

  • 3)const_cast: 用于去除const常量属性,使其可以修改 ,也就是说,原本定义为const的变量在定义后就不能进行修改的,但是使用const_cast操作之后,可以通过这个指针或变量进行修改; 另外还有volatile属性的转换。

  • 4)reinterpret_cast 几乎什么都可以转,用在任意的指针之间的转换,引用之间的转换,指针和足够大的int型之间的转换,整数到指针的转换等。但是不够安全。

12 运算符重载

参考:运算符重载

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值