面试
-
- [1. 指针、引用](#1. 指针、引用)
- [2. 数组](#2. 数组)
- [3. 缺省参数](#3. 缺省参数)
- [4. 函数重载](#4. 函数重载)
- [5. 内联函数](#5. 内联函数)
- [6. 宏](#6. 宏)
- [7. auto](#7. auto)
- [8. const](#8. const)
- [9. 类和对象](#9. 类和对象)
- [10. 类的6个默认成员函数](#10. 类的6个默认成员函数)
- [11. 封装、继承、多态](#11. 封装、继承、多态)
- [12. 结构体内存对齐规则](#12. 结构体内存对齐规则)
- [13. this指针](#13. this指针)
- [14. 初始化列表](#14. 初始化列表)
- [15. explicit](#15. explicit)
- [16. static](#16. static)
- [17. 友元类、友元函数](#17. 友元类、友元函数)
- [18. 内部类](#18. 内部类)
- [19. 内存管理](#19. 内存管理)
- [20. 堆上开辟空间(malloc、calloc、realloc、free)](#20. 堆上开辟空间(malloc、calloc、realloc、free))
- [21. new、delete操作符](#21. new、delete操作符)
1. 指针、引用
指针: 一个实体,存放指向对象地址的变量。指向一块内存,指向可以改变,有const和非const的区别,甚至可以为空(NULL)。
引用: 变量(内存)的别名,一经定义不可修改,且必须初始化。
c
int A=10;
int& rA = A; //引用
int* pA = &A; // 指针
char* p = "hello"; //字符指针
int* arr[10]; //指针数组
int (*p)[10]; //数组指针
int *p = &A; //整形指针
float f = 1.0; float* pf = &f; //单精度浮点型指针
double d = 2.00; double* pd = &d; //双精度浮点型指针
注: 指针与对应的变量类型保持一致。若类型不匹配可能存在以下问题:
- 错误的数据解释。解引用出错。
- 内存访问错误。触发违例,访问错误。
- 类型安全问题。破坏类型安全性,难以调试和理解。
野指针(wild): 没有经过初始化的指针。
悬空指针(dangling): 指针指向已经被释放的的内存空间的指针。
规避野指针:
- 指针创建立即初始化。
- 使用指针过程中,防止指针越界访问。
- 指针指向的空间释放,指针立即置为NULL。
- 使用指针前进行安全检查。
🔥 指针和引用的区别:
-- | 指针 | 引用 |
---|---|---|
NULL | 存在空(NULL)指针 | 无空引用 |
初始化 | 定义可不初始化,使用再进行初始化 | 定义必须初始化 |
指向 | 指向可以改变 | 初始化完毕,指向不可更改,不可引用其他实体 |
多级 | 存在多级指针(如:二级指针等) | 无多级引用 |
访问 | 需要显式解引用,才能获取值(如:*p) | 编译器处理,无须显式解引用 |
参数 | 实质是传值,传递的值是指针的地址 | 实质是传地址,传递的是变量的地址 |
sizeof | 32位操作系统:4Byte 64位操作系统:8Byte | 实体类型的大小(如:int 4Byte) |
自增+1 | 指针向后偏移一个类型大小 | 实体自增+1 |
安全性 | 存在野指针,安全性比引用差 | 安全性好 |
2. 数组
定义:数组是一种数据结构,用于存储固定大小的同类型元素的集合。每个元素可以通过索引访问,索引从0开始。
特点:
- 固定大小:数组的大小在声明时指定,并且在运行时不能更改。
- 内存连续性:数组中的元素在内存中是连续存储的。
- 索引访问:通过索引可以快速访问数组中的任何元素(索引访问:O(1))。
注:
1.越界访问:访问数组的非法索引会导致未定义行为,C++不会自动检查数组边界。
2.数组与指针:在C++中,数组名通常被当作指针来处理,指向数组的第一个元素。
3. 缺省参数
缺省参数: 声明或者定义函数时,函数的参数有默认值。
cpp
// C语言不支持,C++支持
void func1(int x=1, int y=2){} //全缺省
void func2(int x, int y=2){} //半缺省
int main()
{
func1();
func2(1);
}
规则:
5. 缺省参数必须是从右往左,连续给值,不能间隔。
6. 缺省参数不能同时出现在声明和定义中。
7. 缺省值是常量 /全局变量。
4. 函数重载
定义 :同一作用域内,函数名称相同,参数列表(参数类型,参数个数,顺序)不同,构成函数重载。
原理 :由于C++ 底层的重命名机制,将函数根据参数的个数,类型,返回值类型做了重命名。
C++底层重命名机制(Name Mangling) :
为了支持函数重载,C++编译器采用了一种称为"名称重整"(Name Mangling)的技术。名称重整是指在编译过程中,编译器将每个函数的名称和参数类型编码为一个唯一的标识符,这样在生成目标代码时,即使是同名的函数,也会有不同的符号名,以避免冲突。
5. 内联函数
定义:inline修饰的函数,编译时代码展开,提升程序运行的效率(以空间换时间)。
适用性:不适合长代码,递归,循环。(不建议声明和定义分开,会导致链接错误)。
6. 宏
优点 | 缺点 |
---|---|
1. 增强代码复用性 2. 提高性能 | 1. 不方便调试(预编译宏替换) 2. 导致代码可读性差,可维护性差,容易误用 3. 没有类型安全检查 |
其他技术替换宏:
- 常量定义使用const。
- 函数定义使用内联函数。
7. auto
auto: auto 关键字是 C++11 引入的一项功能,它用于自动推导变量的类型。处理 STL 容器和迭代器时特别有用,因为它可以简化代码并减少冗长的类型声明。
类型推导规则:
1.单一变量:根据初始化表达式的类型来推导变量的类型。
2.多个变量:所有变量的类型都将根据第一个变量的初始化表达式进行推导。
注:
1.如果初始化值是 const
或引用,auto 会推导成相应的 const 或引用类型。
2.auto
默认情况下推导出的类型是值类型,如果需要引用类型,可以使用 auto&
或 const auto&
。
8. const
const: 在C++中,const关键字用于定义常量,表示值不可修改。它可以用于变量、指针、函数参数和类成员变量,成员函数等不同场景。
-
使用
const
关键字定义的变量在初始化后不能被修改。 -
指针和指针指向的值都可以使用
const
关键字。
指向常量的指针 : 指针本身可以修改,但不能通过指针修改它指向的值。cppconst int* ptr = &x; // 指向常量的指针 // *ptr = 20; // 错误:不能修改指向的值 int y = 30; ptr = &y; // 合法:可以改变指针指向
常量指针 : 指针本身是常量,不能修改指向的地址,但可以通过指针修改指向的值。
cppint z = 40; int* const ptr2 = &z; // 常量指针 *ptr2 = 50; // 合法:可以修改指向的值 // ptr2 = &y; // 错误:不能修改指针指向
指向常量的常量指针 : 指针本身和它指向的值都不能修改。
cppconst int* const ptr3 = &x; // 指向常量的常量指针 // *ptr3 = 60; // 错误:不能修改指向的值 // ptr3 = &y; // 错误:不能修改指针指向
-
const
成员函数表示不会修改对象的状态,即成员变量的值。 -
const
可以用于函数参数和返回类型,以确保在函数内部不修改传入的参数。 -
类中使用
const
定义的成员变量必须在初始化列表中初始化。
9. 类和对象
类: 类是一个用户定义的数据类型,它描述了对象的属性和行为。类定义了对象的结构和方法,是对象的蓝图。类描述了一组有相同特性(属性)和相同行为的对象。
对象: 对象是类的实例化,是实际存在的实体。通过对象可以访问类的属性和方法。
在C++中,类成员可以有不同的访问权限。
访问控制:
public
: 公有成员,类外部可以访问。private
: 私有成员,只有类的内部可以访问。protected
: 受保护成员,只有类的内部和子类可以访问。
注:class
默认是private
,struct
默认是public
。
10. 类的6个默认成员函数
-
默认构造函数 (Default Constructor):不带参数的构造函数。
说明 :默认构造函数是在没有参数的情况下创建对象时调用的。如果类没有定义任何构造函数,编译器会自动生成一个默认构造函数。
构造函数的作用 :初始化对象,当对象创建时调用构造函数。cppclass MyClass { public: MyClass() {} // 默认构造函数 }; int main() { MyClass obj; // 调用默认构造函数 return 0; }
-
析构函数 (Destructor):用于在对象生命周期结束时清理资源。
说明 :析构函数用于在对象生命周期结束时执行清理操作。它的名称前有一个波浪号(~),并且没有参数和返回值。
调用阶段 :- 当对象生命周期结束时调用析构函数。
- 对于栈上的对象(局部变量),当离开作用域时调用析构函数。
- 对于堆上的对象,当使用delete运算符时调用析构函数。
- 对于全局对象和静态对象,在程序结束时调用析构函数。
cppclass MyClass { public: ~MyClass() {}// 析构函数 }; int main() { MyClass obj; // 对象生命周期结束时调用析构函数 return 0; }
-
拷贝构造函数 (Copy Constructor):用于通过另一个同类型对象初始化新对象。
说明 :拷贝构造函数用于通过另一个同类型的对象初始化新对象。它的参数是一个对同类型对象的常量引用。cppclass MyClass { public: MyClass(const MyClass& other) {} // 拷贝构造函数 }; int main() { MyClass obj1; MyClass obj2 = obj1; // 调用拷贝构造函数 return 0; }
-
赋值运算符重载 (Copy Assignment Operator):用于将一个对象赋值给另一个同类型对象。
说明 :赋值运算符用于将一个对象赋值给另一个同类型的对象。它返回对当前对象的引用。cppclass MyClass { public: MyClass& operator=(const MyClass& other) { if (this != &other) { // 拷贝赋值逻辑 } return *this; } }; int main() { MyClass obj1; MyClass obj2; obj2 = obj1; // 调用拷贝赋值运算符 return 0; }
-
移动构造函数 (Move Constructor):用于通过另一个同类型的右值对象(临时对象)初始化新对象。
说明 :移动构造函数用于通过另一个同类型的右值对象(临时对象)初始化新对象。它的参数是一个对同类型对象的右值引用。cppclass MyClass { public: MyClass(MyClass&& other) noexcept { // 移动构造函数 } }; int main() { MyClass obj1; MyClass obj2 = std::move(obj1); // 调用移动构造函数 return 0; }
-
移动赋值运算符 (Move Assignment Operator):用于将一个同类型的右值对象(临时对象)赋值给另一个对象。
说明 :移动赋值运算符用于将一个同类型的右值对象(临时对象)赋值给另一个对象。它返回对当前对象的引用。cppclass MyClass { public: MyClass& operator=(MyClass&& other) noexcept { if (this != &other) { // 移动赋值逻辑 } return *this; } }; int main() { MyClass obj1; MyClass obj2; obj2 = std::move(obj1); // 调用移动赋值运算符 return 0; }
11. 封装、继承、多态
C++的三大特性,封装,继承,多态。
封装 :是将数据(成员变量)和操作数据的函数(成员函数)包装在一个类中,从而实现对数据的保护和操作的统一。封装提供了数据隐藏和接口暴露两个重要功能,确保对象的内部状态只能通过公开的接口进行操作,从而提高了代码的安全性和可维护性。
继承 :是从一个已有的类(基类或父类)创建一个新类(派生类或子类)的机制。派生类继承了基类的所有非私有成员(属性和方法),并可以扩展或重写这些成员。继承允许代码的重用和类的扩展。
多态 :是指同一操作或方法调用在不同对象上可以表现出不同的行为。多态可以通过虚函数和继承实现。主要有两种多态:
- 静态多态(编译时多态):通过函数重载和运算符重载实现。
- 动态多态(运行时多态):通过虚函数和继承实现。
🔥总结:
- 封装:通过隐藏类的内部实现细节,只暴露公共接口,提高了数据安全性和代码的可维护性。
- 继承:允许从已有的类创建新类,支持代码重用和类的扩展。
- 多态:允许通过相同的接口调用不同的实现,支持运行时动态绑定和扩展功能。
12. 结构体内存对齐规则
结构体的内存对齐规则旨在提高内存访问效率。内存对齐涉及如何将结构体的成员变量排列在内存中的问题,以满足特定的对齐要求。
内存对齐的基本概念:
-
对齐(Alignment):每个数据类型都有一个对齐要求,即它们在内存中必须按特定的字节边界对齐。例如,4字节对齐意味着数据的地址必须是4的倍数。
-
填充(Padding):为了满足对齐要求,编译器在结构体成员之间和末尾可能会插入一些未使用的字节,这些字节称为填充字节。
对齐规则:
-
每个成员的偏移量必须是其对齐大小的倍数。对齐大小通常是成员大小,但有时也可以是编译器指定的对齐值。
-
结构体的总大小必须是其最大对齐成员的倍数 :为了确保数组中的每个结构体实例都正确对齐,结构体的总大小也会被填充到其最大对齐成员的倍数。
cppstruct Example { char a; // 1字节 int b; // 4字节 short c; // 2字节 float d; // 4字节 double e; // 8字节 }; //24
优缺点:
优点 | 缺点 |
---|---|
1. 提高内存访问效率,尤其在硬件对齐要求严格的体系结构上。 2. 减少缓存未命中(cache miss)的概率,提高缓存利用率。 | 1. 增加内存消耗,填充字节占用额外的内存空间。 |
13. this指针
this 指针 :C++中的一个特殊指针,它指向调用成员函数的对象本身。每个成员函数都有一个隐含的参数 this,这个参数是一个指向当前对象的指针。通过 this 指针,成员函数可以访问调用它的对象的成员变量和其他成员函数。
用途:
- 访问成员变量和成员函数:this 指针可以用于在成员函数内部访问对象的成员变量和成员函数。
- 返回对象本身:this 指针可以用于在成员函数中返回当前对象的引用或指针,以支持链式调用。
- 区分成员变量和参数:在成员函数的参数名与成员变量名相同时,可以使用 this 指针区分它们。
14. 初始化列表
初始化列表 :在C++中,初始化列表是一种用于在构造函数中初始化类成员的语法。
优点:
- 提高效率:通过初始化列表,成员变量在对象创建时直接初始化,而不是先调用默认构造函数然后再赋值,这样可以避免不必要的赋值操作,提高效率。
- 支持常量成员和引用成员的初始化:常量成员和引用成员必须在初始化列表中初始化,因为它们在创建后不能被赋值。
- 支持无默认构造函数的成员初始化:如果成员变量的类型没有默认构造函数,它们必须在初始化列表中显式初始化。
注 :初始化顺序,按照声明顺序,而不是初始化列表中的书写顺序。
15. explicit
在C++中,explicit
关键字用于修饰构造函数 ,目的是 防止编译器在不经意间进行隐式类型转换 。默认情况下,C++允许通过构造函数进行隐式类型转换,这可能导致一些潜在的错误或不明确的行为。
作用:
- 防止隐式转换:explicit关键字告诉编译器,构造函数不应该被用作隐式转换的手段,只能通过显式调用来使用。
- 增强代码可读性:使用explicit可以使代码更加清晰,避免在类型转换时产生意外的结果。
16. static
在C语言和C++中,static关键字都有多个作用,但在C++中的应用更广泛。
相同与差异:
- 静态局部变量:定义在函数内部的局部变量,使用static关键字修饰后,这个变量在函数调用之间保持其值不变。
- 静态全局变量:定义在文件内部的全局变量,使用static关键字修饰后,这个变量的作用域仅限于定义它的文件。
- 静态函数:定义在文件内部的函数,使用static关键字修饰后,这个函数的作用域仅限于定义它的文件。
- 类的静态成员变量:属于整个类,而不是某个特定的对象。所有对象共享同一个静态成员变量。
- 类静态成员函数:类的静态成员函数不依赖于具体对象,可以通过类名直接调用。
C | C++ |
---|---|
1. 静态局部变量 2. 静态全局变量 3. 静态函数 | 1. 静态局部变量 2. 静态全局变量 3. 静态函数 4. 类的静态成员变量 5. 类的静态成员函数 |
总结 :C++中做了扩展,增加了定义类的静态成员变量和静态成员函数。
17. 友元类、友元函数
C++中,友元函数(Friend Function)和友元类(Friend Class)是用于访问类的私有和保护成员的机制。它们提供了一种方式来允许特定的函数或类访问其他类的私有和保护成员。
友元类 :是一个被特定类声明为友好的类,这样友元类的所有成员函数都可以访问被声明为友好的类的私有和保护成员。
友元函数 :是一个被特定类声明为友好的函数,声明时需要加friend关键字 。它可以访问该类的所有私有(private)和保护(protected)成员。
- 友元函数不能用const修饰。
- 友元函数可以在类定义的任何地方声明,不受类访问限定符限制。
- 友元函数不是该类的成员函数,但它们可以访问该类的内部数据。
- 一个函数可以是多个类的友元函数。
- 友元函数的调用与普通函数的调用和原理相同。
友元关系的特点:
- 友元关系是单向的,不具有交换性,不能传递。(A是B的友元函数,A同时是C的友元函数,但是B和C不是友元关系)。
- 避免滥用友元函数和友元类,在一定程度上破坏了类的封装特性。