以下内容相关均为面试中碰到的问题,或牛客知乎B站上,面经总结。
其中部分回答是询问AI回答得知,若有差错谬误,还望指出。
以后如果写的多了,会分块,目前先全部放在一起。
当使用<...>
尖括号形式时,预处理器会在标准系统目录中搜索头文件。这种方式主要用于包含标准库提供的头文件,如 <iostream>
、<vector>
等。
当使用"..."
双引号形式时,预处理器首先会在当前源文件所在的目录中搜索头文件。如果没有找到,则会继续在标准系统目录中搜索。这种方式主要用于包含用户自定义的头文件,如项目内的头文件或第三方库的头文件。
#include <iostream>
int main() {
int value1 = 10;
int value2 = 20;
// 指针常量
const int* ptrConst = &value1;
std::cout << "Value through const pointer: " << *ptrConst << std::endl;
// 常量指针
int* const ptr = &value1;
std::cout << "Value through constant pointer: " << *ptr << std::endl;
// 通过常量指针修改数据
*ptr = 30;
std::cout << "New value through constant pointer: " << *ptr << std::endl;
// 尝试改变常量指针的值
// ptr = &value2; // 编译错误
return 0;
}
void Swap(int& a, int& b)
{
int t = a;
a = b;
b = t;
}
int main(int argc, char** argv)
{
int a, b;
a = 200;
b = 300;
Swap(a, b);
printf("%d %d\n", a, b);
return 0;
}
引用的本质在c++内部实现是一个常量指针.
数据类型 * const ref = &value;
inline 返回值类型 函数名(){函数体}
编译器把内存分为三个部分:
1.静态存储区域:主要保存全局变量和静态变量。生存期:整个程序。
2.堆:存储动态生成的变量。生存期:自己来决定。
3.栈:存储调用函数相关的变量和地址等。生存期:所处的语句块(既{}的范围)
1、申请方式的不同。栈由系统自动分配,而堆是人为申请开辟;
2、申请大小的不同。栈获得的空间较小,而堆获得的空间较大;
3、申请效率的不同。栈由系统自动分配,速度较快,而堆一般速度比较慢;
4、存储内容的不同。栈在函数调用时,函数调用语句的下一条可执行语句的地址第一个进栈,然后函数的各个参数进栈,其中静态变量是不入栈的。而堆一般是在头部用一个字节存放堆的大小,堆中的具体内容是人为安排;
5、底层不同。栈是连续的空间,而堆是不连续的空间。
栈的大小是有限制的,在编译时可以设置默认值
指针函数和函数指针是两种不同的概念,它们在C/C++编程中扮演着重要的角色。下面是这两者之间的主要区别:
指针函数是一种特殊的函数,它的返回值是一个指针。这种函数通常用于动态内存分配(如malloc
、calloc
等)或者是为了返回某个特定类型的指针给调用者。指针函数的定义包括了返回类型(指针类型),函数名称,以及参数列表。
例如,一个返回指向整数的指针的指针函数可以这样定义:
int* createInt(int value) {
int *ptr = malloc(sizeof(int));
if(ptr!= NULL) {
*ptr = value;
}
return ptr;
}
在这个例子中,createInt
函数接受一个整数值作为参数,并返回一个指向该整数的指针。
函数指针是一种变量,其值为另一个函数的地址。函数指针允许你将函数作为参数传递给其他函数,或者存储函数的引用以便稍后调用。函数指针的定义包括了函数的原型(返回类型、函数名和参数列表)。
例如,定义一个函数指针来指向一个接受两个整数并返回一个整数的函数:
int add(int a, int b) {
return a + b;
}
int (*funcPtr)(int, int) = add;
在这个例子中,funcPtr
是一个函数指针,它指向add
函数。通过funcPtr
,你可以调用add
函数,就像直接调用add
函数一样。
总结来说,指针函数关注于返回一个指针,而函数指针关注于存储或传递函数的引用。指针函数通常用于动态内存管理或返回特定类型的指针,而函数指针提供了一种灵活的方式来操作函数,允许你将函数作为参数传递或存储函数引用以便稍后调用。
联合体(union)和结构体(struct)都是C/C++中用于组合多个不同类型的数据的数据结构,但它们在使用和行为上的主要区别如下:
struct Person {
char name[50];
int age;
float salary;
};
union Data {
char c;
int i;
float f;
};
在这个联合体示例中,Data
可以存储一个字符、一个整数或一个浮点数,但任何时候只能存储其中一种类型的数据。
进程和线程是计算机操作系统中两种基本的执行单位,它们在功能、资源分配和执行方式上有一些关键的区别。
总结来说,进程和线程都是计算机操作系统中执行程序的基本单位,但它们在资源分配、执行方式和开销上有明显的区别。进程提供了隔离的执行环境,而线程则允许在同一进程内并发执行多个任务,提高了程序的执行效率。
int
),++a
操作在多线程环境下不是线程安全的,因为它是非原子的。std::atomic<int>
),++a
操作是线程安全的,因为它是原子性的。++
操作符重载涉及复杂的逻辑或多个成员变量的修改,你需要显式地使用同步机制来确保线程安全。原子操作是在多线程或多处理器环境中保证数据一致性和完整性的重要手段。
‘编译链接过程是将人类可读的源代码转换为机器可执行的程序的关键步骤。在C语言编程中,这个过程通常分为四个主要阶段:预处理、编译、汇编和链接。以下是每个阶段的详细说明:
#include
、#define
等,展开宏、处理条件编译等。.i
文件。.s
文件。.o
文件)。.o
文件)。.exe
或.out
等)。总结来说,编译链接过程是将源代码转换为可执行程序的关键步骤,包括预处理、编译、汇编和链接四个阶段。预处理阶段处理源代码中的预处理指令;编译阶段将预处理后的代码转换为汇编代码;汇编阶段将汇编代码转换为机器指令;链接阶段将多个目标文件、操作系统的启动代码和库文件组织成最终的可执行文件。链接过程中可能涉及到动态链接和静态链接,根据需要选择合适的链接方式。
动态加载(DLC)通常指的是动态链接的内容,尤其是在游戏和其他软件中,动态加载的内容可以是游戏关卡、皮肤、扩展包等。这些内容通常不是程序的核心部分,而是可以单独下载和安装的附加内容。
动态链接和静态链接各有优缺点,选择哪种链接方式取决于具体的应用场景和需求:
用宏定义写出swap(x,y)
#define SWAP(a, b) do { int temp = (a); (a) = (b); (b) = temp; } while(0)
//这里涉及到do while(false)的妙用
用宏定义写出min(x,y)
#define min(x, y) (((x) < (y)) ? (x) : (y))
auto
)允许使用auto
关键字来自动推导变量的类型,这在处理复杂的类型或模板类型时非常有用。
std::vector<int> v = {1, 2, 3};
auto it = v.begin(); // 编译器推断it为std::vector<int>::iterator
提供了一种简洁的方式来遍历容器。
std::vector<int> numbers = {1, 2, 3};
for (auto& num : numbers) {
std::cout << num << std::endl;
}
std::shared_ptr
, std::unique_ptr
, std::weak_ptr
)用于自动管理动态分配的内存,防止内存泄漏。
std::unique_ptr<int> uptr(new int(10));
std::shared_ptr<int> sptr(new int(20));
std::weak_ptr<int> wptr(sptr);
允许开发者在需要的地方快速定义匿名函数。
std::vector<int> vec = {3, 1, 4, 1, 5};
std::sort(vec.begin(), vec.end(), [](int a, int b){ return a > b; });
允许更高效的资源管理,特别是在对象的复制和移动操作中。
class MyClass {
public:
MyClass(MyClass&& other) noexcept : value(other.value) {
other.value = 0; // 夺取other的资源
}
private:
int value;
};
替代了旧的NULL
宏定义,提供一个明确的空指针类型。
int* ptr = nullptr;
constexpr
允许在编译时计算函数的结果。
constexpr int square(int x) { return x * x; }
static_assert(square(5) == 25); // 在编译时期就能知道结果
简化了对象的初始化。
struct Point {
double x, y;
Point(double x, double y) : x(x), y(y) {}
};
Point p{1.0, 2.0}; // 统一初始化
<thread>
, <mutex>
, <condition_variable>
)提供了标准的多线程支持,简化了并发编程。
#include <thread>
void hello() { std::cout << "Hello from thread" << std::endl; }
int main() {
std::thread t(hello);
t.join();
return 0;
}
class NoCopy {
public:
NoCopy() {} // 默认构造函数
// 删除拷贝构造函数
NoCopy(const NoCopy&) = delete;
// 删除拷贝赋值运算符
NoCopy& operator=(const NoCopy&) = delete;
};
// 下面的代码会导致编译错误
// NoCopy obj1;
// NoCopy obj2 = obj1; // 尝试调用已删除的拷贝构造函数
还有许多其他有用的特性,比如static_assert
、noexcept
、统一初始化、扩展的Unicode字符串字面量、alignof
、noexcept
运算符、override
和final
关键字等等。
建议挑几个好记的记,可以从其延伸回答,并且问的多的。
Lambda 表达式是 C++11 引入的一种简洁的定义匿名函数的方法。Lambda 表达式允许你在代码中定义并使用一次性函数,而不必显式地定义一个函数。
Lambda 表达式的语法如下:
[lambdacapture] (parameters) mutable -> returntype { body }
[capture]
:捕获列表,用于指定哪些变量可以被 lambda 函数捕获。(parameters)
:参数列表。mutable
:可选关键字,用于指定 lambda 函数是否可以修改捕获的变量。-> returntype
:可选的返回类型指定。{ body }
:函数体。#include <iostream>
int main()
{
int x = 10;
// 定义一个 lambda 函数
auto lambda = [x]() mutable { x += 10; std::cout << "x = " << x << std::endl; };
lambda(); // 输出:x = 20
std::cout << "Original x = " << x << std::endl; // 输出:Original x = 10
return 0;
}
#include <iostream>
int main()
{
int x = 10;
int a = 5;
// 定义一个 lambda 表达式,并使用 auto 接收
auto lambda = []() { return 0; };
auto lambdaX = [x]() { return x; };
auto lambdaXA = [a, x]() { return x + a; };
auto lambdaXA_BC = [a, x](int b, int c) {return a + x + b + c; };
// 求 lambda 变量的大小
std::cout << "Size of lambda: " << sizeof(lambda) << " bytes" << std::endl; //输出为1bytes 可以看作函数指针的大小
std::cout << "lambdaX: " << sizeof(lambdaX) << std::endl; //输出为4bytes
std::cout << "lambdaXA: " << sizeof(lambdaXA) << std::endl;//输出为8bytes
std::cout << "lambdaXA_BC: " << sizeof(lambdaXA_BC(1, 2)) << std::endl; //输出为4byt是因为lambdaXA_BC(1,2)的返回值是int类型
return 0;
}
输出结果可能因编译器和平台的不同而有所不同。
lambda
)lambda
是一个没有捕获任何变量的 lambda 表达式。sizeof(lambda)
的结果可能是非常小的,如 1 字节,这通常表示一个函数指针的大小。int
变量 (lambdaX
)lambdaX
捕获了一个 int
类型的变量 x
。int
类型在大多数平台上是 4 字节(对于 32 位系统)或 8 字节(对于 64 位系统)。sizeof(lambdaX)
的结果为 4 字节,表示闭包对象包含了一个 int
类型的变量。int
变量 (lambdaXA
)lambdaXA
捕获了两个 int
类型的变量 a
和 x
。int
类型的大小为 4 字节,因此总共需要 8 字节来存储这两个变量。sizeof(lambdaXA)
的结果为 8 字节。需要注意的是,编译器可能会对 lambda 表达式进行优化。例如,对于无捕获的 lambda,编译器可能会将其优化为一个简单的函数指针,因此 sizeof(lambda)
的结果可能非常小。
typedef为C语言的关键字,作用是为一种数据类型(基本类型或自定义数据类型)定义一个新名字,不能创建新类型。
char* 和 char[] 的区别
函数参数中的 char* 和 char[] 的区别
当作为函数参数时,char*
和 char[]
都会被退化为指向第一个元素的指针类型。因此,它们在形式上看起来是相同的。但是,从语义上来讲,它们表示的是不同的东西:
char* param
表明参数是一个指向字符的指针。char param[]
虽然编译器处理成 char* param
,但它暗示参数是一个字符数组。实际使用时,通常根据习惯或者意图来选择哪种写法。例如,如果你希望强调这是一个数组,那么可能会使用 char[]
形式;如果强调这是个指针,则使用 char*
形式。
智能指针是一种具有自动资源管理功能的指针,如 std::unique_ptr
, std::shared_ptr
,std::weak_ptr
能够自动管理内存生命周期。
unique_ptr相比auto_ptr禁用了拷贝构造,采用了移动赋值来进行所有权的转移
1. 所有权语义明确
std::unique_ptr
明确表示了独占所有权,意味着一个 std::unique_ptr
对象不能被复制,只能通过移动语义来转移所有权。这减少了由于意外的拷贝而导致的资源管理错误。std::auto_ptr
不同,std::unique_ptr
不可被拷贝,因此不会有意外的资源所有权转移。如果需要转移所有权,必须显式地使用移动语义(通过 std::move
)。2. 线程安全性
std::unique_ptr
在内部使用了线程安全的方式来管理资源,特别是当涉及到析构和释放资源时。而 std::auto_ptr
在某些情况下可能会有线程安全问题,尤其是在析构函数的调用顺序上。std::unique_ptr
是一种独占所有权的智能指针,它确保同一时间内只有一个 std::unique_ptr
对象拥有某个资源。这意味着:
std::unique_ptr
不能被复制,以确保任何时候都只有一个对象拥有资源的所有权。std::unique_ptr
可以通过移动语义来转移所有权。这意味着可以通过 std::move
将一个 std::unique_ptr
的所有权转移到另一个 std::unique_ptr
。std::unique_ptr
对象超出作用域或被显式销毁时,它所管理的资源会被自动释放。std::unique_ptr
支持自定义删除器,可以用来执行额外的清理工作,如关闭文件句柄等。#include <memory>
#include <iostream>
int main()
{
std::unique_ptr<int> ptr(new int(10)); // 创建一个 unique_ptr 指向整型
std::cout << *ptr << std::endl; // 输出: 10
// ptr 不可复制,但可以通过移动语义转移所有权
std::unique_ptr<int> anotherPtr = std::move(ptr);
std::cout << *anotherPtr << std::endl; // 输出: 10
// 当 anotherPtr 超出作用域时,其所指向的整型会被自动删除
return 0;
}
std::shared_ptr
是一种共享所有权的智能指针,它可以允许多个 std::shared_ptr
对象共享同一个资源。它通过引用计数来跟踪有多少个 std::shared_ptr
正在使用同一个资源:
std::shared_ptr
可以被复制,每复制一次就会增加引用计数。std::shared_ptr
管理的资源会被释放。std::shared_ptr
彼此相互持有对方,则可能会导致引用计数永远不降为零,从而导致内存泄漏。此时可以使用 std::weak_ptr
来打破循环引用。#include <memory>
#include <iostream>
class A
{
public:
std::shared_ptr<B> b;
~A() { std::cout << "A destructed" << std::endl; }
};
class B
{
public:
std::shared_ptr<A> a;
~B() { std::cout << "B destructed" << std::endl; }
};
int main()
{
auto a = std::make_shared<A>();
auto b = std::make_shared<B>();
a->b = b;
b->a = a;
// 由于循环引用,如果没有 weak_ptr,这两个对象都不会被删除
// 使用 std::weak_ptr 可以解决这个问题
return 0;
}
另外:
std::shared_ptr
的引用计数操作是线程安全的,这意味着你可以安全地在多个线程中创建、赋值和拷贝std::shared_ptr
std::shared_ptr
访问或修改其所指向的对象时,你需要自己负责同步,以避免数据竞争和不一致性问题。std::weak_ptr :
是一种弱引用的智能指针,它不增加引用计数,主要用于解决 std::shared_ptr
导致的循环引用问题:
std::weak_ptr
不拥有资源,也不会增加资源的引用计数。std::weak_ptr::expired()
方法来检查是否还有 std::shared_ptr
持有资源。std::shared_ptr
:如果资源仍然存在,可以使用 std::weak_ptr::lock()
方法来获取一个 std::shared_ptr
。#include <memory>
#include <iostream>
class Node
{
public:
std::shared_ptr<Node> next;
std::weak_ptr<Node> prev;
~Node() { std::cout << "Node destructed" << std::endl; }
};
int main()
{
auto head = std::make_shared<Node>();
auto tail = std::make_shared<Node>();
head->next = tail;
tail->prev = head;
// 使用 weak_ptr 避免循环引用
return 0;
}
std::unique_ptr
用于独占资源管理,确保任何时候只有一个拥有者。std::shared_ptr
用于共享资源管理,允许多个拥有者,但可能会导致循环引用。std::weak_ptr
用于辅助解决 std::shared_ptr
的循环引用问题,不增加引用计数。合理选择和使用这三种智能指针可以有效地帮助管理内存,避免内存泄漏和其他与手动管理内存相关的错误。
volatile 关键字主要用于多线程环境中,用于标记一个变量,使得对该变量的读写操作不会被编译器或处理器优化而存储在寄存器中,而是每次都强制从主内存中加载最新的值。
总结,volatile关键字的主要目的是禁止编译器优化,解决内存可见性和有序性问题,但volatile不能保证线程安全,得上锁。
函数内static变量是否只会初始化一次
函数内的static变量在整个程序运行期间只初始化一次。这意味着无论函数被调用多少次,static变量只会被初始化一次,并且在函数退出后保留其值。
#include <iostream>
class MyClass {
public:
static int counter; // 声明静态成员变量
MyClass() {
counter++; // 自增
}
static int getCounter() {
return counter; // 使用静态成员变量
}
};
// 类外部初始化
int MyClass::counter = 0;
int main() {
MyClass obj1;
MyClass obj2;
std::cout << "Counter: " << MyClass::getCounter() << std::endl; // 输出:Counter: 2
return 0;
}
STL 容器大致可以分为以下几类:
vector
, list
, deque
。set
, map
, multiset
, multimap
。stack
, queue
, priority_queue
。unordered_set
, unordered_map
, unordered_multiset
, unordered_multimap
。vector
list
deque
set
和 multiset
list
。set
中的元素唯一,multiset
中可以有重复元素。map
和 multimap
list
。map
中的键唯一,multimap
中可以有重复的键。stack
vector
或 deque
实现。queue
deque
实现。priority_queue
unordered_set
和 unordered_multiset
unordered_set
中的元素唯一,unordered_multiset
中可以有重复元素。unordered_map
和 unordered_multimap
unordered_map
中的键唯一,unordered_multimap
中可以有重复的键。push_back
, push_front
, insert
。pop_back
, pop_front
, erase
。find
, count
, equal_range
。begin
, end
, cbegin
, cend
。sort
, stable_sort
。reverse
。clear
。iterator
, const_iterator
, reverse_iterator
, const_reverse_iterator
。operator*
, operator->
, operator++
, operator--
。vector
在尾部插入和删除最快,list
在任意位置插入和删除最快。vector
内存连续,list
内存分散。set
和 map
查找较快,unordered_set
和 unordered_map
查找更快(平均 O(1))。vector
:适合需要随机访问且元素数量频繁变化的场景。list
:适合需要频繁插入和删除元素的场景。deque
:适合需要两端高效插入和删除元素的场景。set
和 map
:适合需要有序存储且查找效率较高的场景。unordered_set
和 unordered_map
:适合需要快速查找但不要求顺序存储的场景。vector
:插入 O(n),删除 O(n),查找 O(n)。list
:插入 O(1),删除 O(1),查找 O(n)。deque
:插入 O(1),删除 O(1),查找 O(n)。set
和 map
:插入 O(log n),删除 O(log n),查找 O(log n)。unordered_set
和 unordered_map
:插入 O(1),删除 O(1),查找 O(1)(平均情况下)。在 C++ 中,迭代器失效通常发生在容器的状态发生改变,导致迭代器不再指向有效的元素或者其指向的元素的位置发生了变化。
以下是一些常见的迭代器失效的情况:
vector
, deque
)对于序列式容器:
std::vector::erase
会使被删除元素之后的所有迭代器失效。std::vector::insert
会使插入位置之后的所有迭代器失效。std::vector::resize
或 std::vector::reserve
,可能导致迭代器失效。clear()
清空容器会使所有迭代器失效。map
, set
, multiset
)对于关联式容器,由于它们通常基于红黑树实现,因此插入和删除操作不会导致其他元素的位置发生改变。但是:
std::map::erase
会使被删除元素的迭代器失效,但不影响其他迭代器。clear()
清空容器会使所有迭代器失效。list
)对于链表式容器:
clear()
清空容器会使所有迭代器失效。为了避免迭代器失效带来的问题,可以采取以下措施:
std::vector::erase
返回下一个有效迭代器。for-each
循环),而不是逐个迭代器操作。以下是一个简单的示例,展示迭代器失效的情况:
#include <iostream>
#include <vector>
#include <map>
int main() {
std::vector<int> vec = {1, 2, 3, 4, 5};
std::map<int, int> m = {{1, 10}, {2, 20}, {3, 30}};
// vector 的迭代器失效
auto it_vec = vec.begin();
vec.erase(it_vec); // 删除第一个元素后,it_vec 失效
std::cout << "After erasing first element, vec: ";
for (auto it = vec.begin(); it != vec.end(); ++it) {
std::cout << *it << " ";
}
std::cout << std::endl;
// map 的迭代器失效
auto it_map = m.find(2);
m.erase(it_map); // 删除迭代器指向的元素后,it_map 失效
std::cout << "After erasing element with key 2, map: ";
for (const auto &pair : m) {
std::cout << "(" << pair.first << ", " << pair.second << ") ";
}
std::cout << std::endl;
return 0;
}
在这个例子中,可以看到在删除元素后,迭代器失效的情况。在实际编程中,应始终留意迭代器是否仍然有效,并采取适当的措施来避免由此引发的问题。
1、父类到子类的转换,( 进行下行转换,把基类的指针或引用转换为派生类表示)不保证安全。
2、子类到父类的转换,(进行上行转换,把派生类的指针或引用转换成基类表示)保证安全。
3、基本数据类型之间的转换,是否正确需要开发人员来保证。
4、void 类型的空指针,转换成其他类型空指针。
5、可以把任何类型的表达式 转换为 void类型。
static_cast不能转换掉表达式的const、volitale属性。
int i = 10;
float f = static_cast<float>(i); // 将 int 转换为 float
dynamic 支持运行指针时,识别指针类型或所引用的对象。
换句话说,它可以在执行期决定真正的类型,如果基类指针真的指向了子类对象,它便返回指子类对象的指针值,否则,则返回空指针。(所以说到底,他就是比static_cast多了个类型检查)
1、其他三种都是编译时完成的,dynamic_cast是运行时处理的。
2、不能用于基本数据类型的转换。
3、dynamic_cast转换如果成功的话返回的是指向类的指针或引用,转换失败的话则会返回NULL。
4、使用dynamic_cast进行转换的,基类中一定要有虚函数,否则编译不通过,类中存在虚函数,就说明它有想要让基类指针或引用指向派生类对象的情况,此时转换才有意义。
5、在类的转换时,在类层次间进行上行转换时,dynamic_cast和static_cast的效果是一样的。在进行下行转换时,dynamic_cast具有类型检查的功能,比static_cast更安全。
class Base {
public:
virtual ~Base() {}
};
class Derived : public Base {};
int main() {
Base* basePtr = new Derived();
Derived* derivedPtr = dynamic_cast<Derived*>(basePtr); // 安全转换
if (derivedPtr != nullptr) {
// 转换成功
} else {
// 转换失败
}
delete basePtr;
return 0;
}
const_cast用于强制去掉const 特性,但需要特别注意的是const_cast不是用于去除变量的常量性,而是去除指向常数对象的指针或引用的常量性,其去除常量性的对象必须为指针或引用
1、该运算符用来修改类型的const或volatile属性。除了const 或volatile修饰之外, type_id和expression的类型是一样的。
2、常量指针被转化成非常量指针,并且仍然指向原来的对象;
3、常量引用被转换成非常量引用,并且仍然指向原来的对象;
4、常量对象被转换成非常量对象。
const int ci = 10;
int* pi = const_cast<int*>(&ci); // 去除 const 属性
它可以把一个指针转换成一个整数,也可以把一个整数转换成一个指针(先把一个指针转换成一个整数,再把该整数转换成原类型的指针,还可以得到原先的指针值)。
在使用reinterpret_cast强制转换过程仅仅只是比特位的拷贝,也不会进行类型检查,因此在使用过程中需要特别谨慎!
1、改变指针或引用的类型
2、将指针或引用转换为一个足够长度的整型
3、将整型转换为指针或引用类型
char* charPtr = "Hello";
int* intPtr = reinterpret_cast<int*>(charPtr); // 危险的转换
struct 默认权限为 public , class 默认权限为 private
默认情况下,c++编译器至少为我们写的类增加3个函数 :
1.默认构造函数(无参,函数体为空)
2.默认析构函数(无参,函数体为空)
3.默认拷贝构造函数, 对类中非静态成员属性简单值拷贝
如果用户定义拷贝构造函数,c++不会再提供任何默认构造函数
如果用户定义了普通构造(非拷贝),c++不在提供默认无参构造,但是会提供默认拷贝构造
类的访问权限
构造函数
构造函数是一种特殊的成员函数,用于初始化类的对象。构造函数的名字与类名相同,并且没有返回类型。
析构函数
析构函数是一种特殊的成员函数,用于清理类的对象。析构函数的名字是类名前面加上波浪号 ~
。
~
。MyClass
的析构函数为 ~MyClass
。void
也不允许指定。delete
时调用。obj1
和 obj2
,则 obj2
的析构函数先被调用,然后是 obj1
的析构函数。obj1
在 obj2
之前构造,则 obj2
的析构函数先被调用,然后是 obj1
的析构函数。virtual ~BaseClass();
。class MyClass {
public:
MyClass(int& ref) : refMember(ref) {}
private:
int& refMember; // 引用成员变量
};
const
成员变量)必须在构造函数的初始化列表中初始化。class MyClass {
public:
MyClass(int const val) : constMember(val) {}
private:
int const constMember; // 常量成员变量
};
class Base
{
public:
Base(int val) : value(val) {}
private:
int value;
};
class Derived : public Base
{
public:
Derived(int baseVal, int derivedVal) : Base(baseVal), derivedValue(derivedVal) {}
private:
int derivedValue;
};
std::vector
或者自定义类型的对象)需要显式调用构造函数来初始化。class MyClass
{
public:
MyClass(const std::vector<int>& vec) : data(vec.begin(), vec.end()) {}
private:
std::vector<int> data;
};
class MyClass
{
public:
MyClass(int val1, int val2) : member1(val1), member2(member1 + val2) {}
private:
int member1;
int member2;
};
使用初始化列表的情况包括但不限于:
主要区别
拷贝构造函数是一种特殊的构造函数,用于创建一个新对象作为已存在对象的副本。它被调用的情况包括:
赋值运算符
赋值运算符是一个成员函数,用于更新已存在的对象,使其与另一个对象相等。它通常被重载以实现类的赋值操作,并且通常遵循“返回 *this”的约定以便支持连续赋值。
赋值运算符被调用的情况包括:
虚继承的主要目的是确保在多重继承的情况下,基类的对象只被继承一次,而不是多次。这有助于避免基类成员的多份副本带来的数据不一致问题。
c++支持编译时多态(静态多态)和运行时多态(动态多态) 运算符重载和函数 重载就是编译时多态
虚函数是一种成员函数,它允许基类指针或引用来调用派生类中的对应函数,从而实现多态性。
特点:
纯虚函数是一种特殊的虚函数,它在基类中只有声明而没有定义。纯虚函数用于强制派生类必须实现这个函数,否则派生类也不能实例化。
特点:
虚函数表(Vtable):每个含有虚函数的类都有一个虚函数表(Vtable),它是一个指向该类所有虚函数的指针数组。
虚表指针(Vptr):每个对象都有一个虚表指针(Vptr),指向其所属类的虚函数表。
动态绑定:当调用一个虚函数时,编译器会通过对象的虚表指针找到正确的函数地址并调用,而不是在编译时就确定调用哪个函数。这就是动态绑定。
在 C++ 中,通常一个对象只有一个虚表指针(vptr)。但是,在某些特殊情况下,确实可能会有多个虚表指针的情况:
除了虚函数表之外,每个对象还有一个虚函数指针(Virtual Pointer,简称 vptr),它指向该对象所属类的虚函数表。
这个指针通常是一个void*
类型的指针,会占用一定大小的空间(通常为 4 字节或 8 字节,取决于平台的指针大小)。因为虚函数表本身不直接属于对象,但它存储了虚函数的地址信息,所以对于每个对象来说,指向虚函数表的指针是必要的。
下面是一个简单的示例,展示虚函数表和虚函数指针的工作原理:
#include <iostream>
class Base {
public:
virtual void print() {
std::cout << "Base::print()" << std::endl;
}
virtual ~Base() {
std::cout << "Base::~Base()" << std::endl;
}
};
class Derived : public Base {
public:
void print() override {
std::cout << "Derived::print()" << std::endl;
}
~Derived() {
std::cout << "Derived::~Derived()" << std::endl;
}
};
int main() {
Base* basePtr = new Derived();
basePtr->print(); // 动态绑定,调用 Derived::print()
delete basePtr; // 调用 Derived 的析构函数
return 0;
}
构造函数的调用是在编译时确定的,而虚函数的调用是在运行时确定的。如果构造函数是虚函数,那么在派生类中重写构造函数时,编译器将无法知道应该调用哪个构造函数,这会导致编译错误或运行时错误。
抽象类,即纯虚类,类的内部成员函数全部是纯虚函数。
// 使用抽象类,不需要关心子类的具体实现细节,只需要知道,该接口函数调用后
// 会达到预取的效果即可,它就可以为以后开发,提供丰富的可变性
内存泄露是指程序在动态分配内存后未能正确释放,导致这部分内存无法被回收。内存泄露可能导致程序消耗越来越多的内存,最终可能导致性能下降甚至崩溃。以下是一些常见的内存泄露场景:
一句话总结:内存对齐是为了提高数据访问效率,确保数据在特定的边界上开始,一般边界定义为4/8字节做分割
什么是内存对齐?
内存对齐是指数据在内存中的起始地址满足某种特定的要求,通常是要求数据的地址能够被其自然对齐值(Natural Alignment)整除。自然对齐值通常等于数据类型的大小,但有时也可能更大。
为什么需要内存对齐?
这里笔者采用的是VS编译器
不同的编译器结果可能不同,仅供参考
struct S1
{
int b;
char a;
char c;
};
struct S2
{
char a;
int b;
char c;
};
struct S3
{
char a;
char c;
int b;
};
int main()
{
printf("%d\n", sizeof(S1)); 输出 8
printf("%d\n", sizeof(S2)); 输出 12
printf("%d\n", sizeof(S3)); 输出 8
return 0;
}
注意S2 因为 char int char
变成了 4 4 4的字节占据空间,为什么会这样请读者自行思考内存对齐或者查询ai可知
在 32 位系统上,void* 占用 4 字节。
在 64 位系统上,void* 占用 8 字节。
这里笔者采用的是VS编译器
不同的编译器结果可能不同,仅供参考
#include <iostream>
#include <vector>
struct MyStruct {
int i; // 4 字节
short s; // 2 字节
void* ptr; // 4 字节(32 位)或 8 字节(64 位)
bool b; // 1 字节
};
int main()
{
#ifdef _WIN64
std::cout << "64-bit Windows detected" << std::endl;
std::cout << "Size of MyStruct (64-bit): " << sizeof(MyStruct) << " bytes" << std::endl; //输出为24
#else
std::cout << "32-bit Windows detected" << std::endl;
std::cout << "Size of MyStruct (32-bit): " << sizeof(MyStruct) << " bytes" << std::endl; //输出为16
#endif
return 0;
}
在 64 位系统上,void* 指针的大小为 8 字节。我们来逐步分析 MyStruct 的成员对齐情况:
int i
: 占 4 字节short s
: 占 2 字节,为了对齐 void* ptr
,需要 2 字节的填充void* ptr
: 占 8 字节bool b
: 占 1 字节,为了对齐到 8 字节边界,需要 7 字节的填充因此,整个 MyStruct
的大小为 24 字节。
总结:
堆栈溢出一般是什么原因导致的堆栈溢出通常是由以下几种原因导致的:
解决堆栈溢出的方法包括:
通过以上措施,可以有效降低堆栈溢出的风险,保证程序的稳定运行。
递归是一种函数调用自身的编程技术。在递归函数中,函数会不断地调用自身来解决问题的一部分,直到达到基本情况(base case)为止。递归函数的一个典型例子是计算阶乘。
计算阶乘 n!:
int factorial(int n) {
if (n == 0) {
return 1; // 基本情况
} else {
return n * factorial(n - 1); // 递归情况
}
}
尾递归是一种特殊的递归形式,其中递归调用是函数中的最后一个操作。这意味着递归调用之后不再有其他的操作需要执行。尾递归函数可以被优化,使得每次递归调用都不会增加额外的栈空间。
计算阶乘 n!n! 的尾递归版本:
int tailFactorial(int n, int accumulator = 1) {
if (n == 0) {
return accumulator; // 基本情况
} else {
return tailFactorial(n - 1, n * accumulator); // 尾递归情况
}
}
在这个例子中,tailFactorial
函数的递归调用是函数中的最后一个操作,因此它是尾递归。
并不是所有的编程语言都支持尾递归优化。例如,C++ 编译器通常会支持尾递归优化,而 Python 则不支持尾递归优化。
尾递归优化的原理是将递归调用转换为迭代操作。每次递归调用时,编译器会用一个新的参数值替换当前的参数值,而不是在栈上分配新的空间。这样可以避免栈溢出。
int factorial(int n) {
if (n == 0) {
return 1;
} else {
return n * factorial(n - 1);
}
}
int main() {
int result = factorial(10000); // 可能导致栈溢出
return 0;
}
int tailFactorial(int n, int accumulator = 1) {
if (n == 0) {
return accumulator;
} else {
return tailFactorial(n - 1, n * accumulator);
}
}
int main() {
int result = tailFactorial(10000); // 不会导致栈溢出
return 0;
}
递归和尾递归都是解决递归问题的有效方法,但尾递归通常更加高效和安全,因为它可以避免栈溢出,并减少函数调用的开销。尾递归可以通过编译器优化来转换为迭代形式,从而提高程序的性能。
new
分配内存时,操作系统可能只是标记了一块虚拟内存区域,但是并没有实际分配物理内存。只有当访问这块内存中的某个地址时,操作系统才会触发 缺页中断(page fault),从而分配物理内存页。放几个常用的排序写法
快排
#include<iostream>
void QuickSort(int* arr, int begin, int end)
{
// 只有一个数或区间不存在
if (begin >= end)
return;
int left = begin;
int right = end;
// 选左边为key
int keyi = begin;
int pivot = arr[keyi]; // 添加pivot变量来存储基准值
while (begin < end)
{
// 右边选小于pivot的值
while (arr[end] >= pivot && begin < end)
{
--end;
}
// 左边选大于pivot的值
while (arr[begin] <= pivot && begin < end)
{
++begin;
}
// 小的换到右边,大的换到左边
int temp = arr[begin];
arr[begin] = arr[end];
arr[end] = temp;
}
// 把基准值放到正确的位置
int temp = arr[keyi];
arr[keyi] = arr[begin];
arr[begin] = temp;
keyi = begin;
// 分别对左右两个子数组进行递归排序
QuickSort(arr, left, keyi - 1);
QuickSort(arr, keyi + 1, right);
}
int main()
{
int a[] = { 80,30,60,40,20,10,50,70 };
int ilen = sizeof(a) / sizeof(a[0]);
cout << "before sort: ";
for (int i = 0; i < ilen; i++)
{
cout << a[i] << " ";
}
cout << endl;
QuickSort(a, 0, ilen - 1);
cout << "after sort: ";
for (int i = 0; i < ilen; i++)
{
cout << a[i] << " ";
}
cout << endl;
return 0;
}
冒泡排序
#include <iostream>
void bubbleSort(int arr[], int n)
{
bool swapped;
for (int i = 0; i < n - 1; i++)
{
swapped = false;
// 每一轮将未排序的最大元素冒泡到正确的位置
for (int j = 0; j < n - 1 - i; j++)
{
if (arr[j] > arr[j + 1])
{
// 交换 arr[j] 和 arr[j + 1]
std::swap(arr[j], arr[j + 1]);
swapped = true;
}
}
// 如果没有发生任何交换,那么数组已经有序
if (!swapped)
break;
}
}
int main()
{
int a[] = { 80, 30, 60, 40, 20, 10, 50, 70 };
int ilen = sizeof(a) / sizeof(a[0]);
std::cout << "Before sort: ";
for (int i = 0; i < ilen; i++)
{
std::cout << a[i] << " ";
}
std::cout << std::endl;
bubbleSort(a, ilen);
std::cout << "After sort: ";
for (int i = 0; i < ilen; i++)
{
std::cout << a[i] << " ";
}
std::cout << std::endl;
return 0;
}
一个简单的最大堆排序实现基于数组
#include <iostream>
// 函数原型声明
void heapify(int arr[], int n, int i);
void heapSort(int arr[], int n);
int main()
{
int a[] = { 80, 30, 60, 40, 20, 10, 50, 70 };
int ilen = sizeof(a) / sizeof(a[0]);
std::cout << "Before sort: ";
for (int i = 0; i < ilen; i++) {
std::cout << a[i] << " ";
}
std::cout << std::endl;
heapSort(a, ilen);
std::cout << "After sort: ";
for (int i = 0; i < ilen; i++) {
std::cout << a[i] << " ";
}
std::cout << std::endl;
return 0;
}
// 该函数用于维护最大堆的性质
void heapify(int arr[], int n, int i)
{
int largest = i; // 初始化最大值为根
int left = 2 * i + 1; // 左子节点
int right = 2 * i + 2; // 右子节点
// 如果左子节点大于根节点
if (left < n && arr[left] > arr[largest])
largest = left;
// 如果右子节点大于当前最大值
if (right < n && arr[right] > arr[largest])
largest = right;
// 如果最大值不是根节点
if (largest != i)
{
std::swap(arr[i], arr[largest]);
// 递归地修复受影响的子树
heapify(arr, n, largest);
}
}
// 主要的堆排序函数
void heapSort(int arr[], int n)
{
// 建立最大堆
for (int i = n / 2 - 1; i >= 0; i--)
heapify(arr, n, i);
// 一个个从堆顶取出元素
for (int i = n - 1; i >= 0; i--)
{
// 将当前根节点与末尾交换
std::swap(arr[0], arr[i]);
// 调用最大堆修复函数
heapify(arr, i, 0);
}
}
对于确定排序所需的最小比较次数,我们需要了解一些关于排序理论的基础知识。根据比较排序的原理,在最坏的情况下,任何基于比较的排序算法至少需要 O(n log n) 比较次数来完成对 n 个元素的排序。这里的 "O" 表示大O符号,用来描述算法的时间复杂度上限。
对于 10,000 个人进行排序,如果得分是从 0 到 100 的整数,那么一共有 101 种可能的得分。在最好的情况下(即,如果我们可以利用这些得分的离散性),我们可以使用计数排序、桶排序或者基数排序这类线性时间复杂度的算法。对于这种特定情况,如果我们使用计数排序的话,时间复杂度可以降低为 O(n + k),其中 n 是人数(10,000),k 是可能的得分种类数(101)。在这种情况下,算法的运行时间主要取决于列表的长度,而不是得分范围。
但是,如果必须使用基于比较的排序算法(如快速排序、归并排序等),那么即使所有人的得分都相同,也需要至少 O(n log n) 的比较次数来确认这一点。对于 10,000 个人来说,这意味着至少需要大约 10,000 * log2(10,000) ≈ 133,000 次比较。
总结来说:
为了实现一个栈,使得入栈、出栈和返回栈中最小元素的操作时间复杂度均为 O(1),我们可以采用两个栈的方法:一个主栈用于存储所有的元素,另一个辅助栈用于跟踪当前栈中的最小元素。
具体实现如下:
class MinStack {
private:
std::stack<int> mainStack; // 存储所有元素
std::stack<int> minStack; // 存储当前最小元素
public:
// 入栈操作
void push(int x) {
mainStack.push(x);
// 如果辅助栈为空,或者新元素小于等于辅助栈的顶部元素,则将其推入辅助栈
if (minStack.empty() || x <= minStack.top()) {
minStack.push(x);
}
}
// 出栈操作
void pop() {
if (mainStack.empty()) {
throw std::runtime_error("Stack is empty.");
}
// 如果要弹出的元素等于辅助栈的顶部元素,则同时弹出辅助栈的顶部元素
if (mainStack.top() == minStack.top()) {
minStack.pop();
}
mainStack.pop();
}
// 获取栈顶元素
int top() {
if (mainStack.empty()) {
throw std::runtime_error("Stack is empty.");
}
return mainStack.top();
}
// 获取栈中的最小元素
int getMin() {
if (minStack.empty()) {
throw std::runtime_error("Stack is empty.");
}
return minStack.top();
}
};
待补充部分-->建议关键字检索其它博客
各类树
例如:
avl树->性质->旋转规律
红黑树->性质->旋转规律
跳表->实现跳表
定时器->实现一个定时器类
时间轮的定时器->实现一个时间轮的定时器类
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。