构造函数:用于在创建对象时初始化对象。它没有返回类型,也不带返回语句。构造函数可以有参数,允许重载。
复制构造函数:用于创建一个新对象作为另一个同类型对象的副本。它接受一个指向同类型对象的引用(通常为const引用)作为参数。如果没有显式定义,编译器会生成一个默认的复制构造函数。
析构函数:用于在对象生命周期结束时进行清理工作,如释放分配的内存。析构函数也没有返回类型,也不带返回语句,且不能被重载。
浅复制:仅复制对象的指针成员,不复制指针指向的内容。因此,原对象和副本对象中的指针成员指向相同的内存地址。
深复制:不仅复制对象的指针成员,还复制指针指向的内容,确保原对象和副本对象完全独立。
数组:在连续的内存区域中分配。每个元素紧挨着前一个元素,便于通过下标访问。
链表:每个节点通常包含数据和指向下一个节点的指针(双向链表则包含两个指针),节点在内存中不必连续。
二叉树:节点可能包含数据和两个指向子节点的指针,节点在内存中也不必连续。
结构体或类占用的内存是其所有成员(包括基类和成员变量)占用内存的总和,还需考虑对齐(padding)。
空类通常占用至少一个字节,以支持对象在内存中的唯一性。
定义:虚函数是在基类中声明,并在派生类中可能被重写的成员函数。通过在函数声明前加上virtual关键字来标记一个函数为虚函数。
作用:虚函数允许在派生类中提供特定于该类的实现,同时保持通过基类指针或引用调用这些函数的灵活性。
定义:虚表是一个由编译器为每个包含虚函数的类自动生成的表。这个表存储了类中所有虚函数的地址。
结构:每个包含虚函数的类的对象(或派生类的对象)都有一个指向其类虚表的指针(通常称为vptr)。这个指针在对象创建时由编译器设置,并指向正确的虚表。
工作原理:当通过基类指针或引用调用虚函数时,程序首先查找该指针或引用所指向对象的vptr,然后通过vptr找到虚表,并在虚表中查找要调用的虚函数的地址。最后,程序使用这个地址来调用相应的函数。
构造函数和析构函数:构造函数不能是虚的,因为对象在构造过程中其类型尚未完全确定,且虚表是在构造函数执行期间设置的。析构函数通常是虚的,以确保通过基类指针删除派生类对象时能够调用正确的析构函数。
虚表继承:当派生类继承自基类时,如果基类有虚函数,派生类会继承基类的虚表,并在需要时扩展它以包含自己的虚函数。如果派生类重写了基类的虚函数,则派生类的虚表中相应函数的地址会被替换为派生类函数的地址。
纯虚函数和抽象类:纯虚函数是没有函数体的虚函数(用= 0标记)。包含至少一个纯虚函数的类被称为抽象类,它不能被实例化。纯虚函数用于在基类中定义一个接口,该接口必须由派生类实现。
虚函数和性能:虽然虚函数提供了强大的多态性支持,但它们可能会引入一些性能开销,因为每次调用虚函数时都需要通过vptr和虚表来查找函数地址。然而,在大多数现代编译器和处理器上,这种开销通常是可以接受的。
时机和原因:
内存泄漏通常发生在动态分配了内存但未释放
分配的内存超出了程序的控制范围(如指针丢失、覆盖)。
如何避免:
使用智能指针(如std::unique_ptr, std::shared_ptr)自动管理内存,确保每次 new 后都有相应的 delete。
仔细检查所有内存分配和释放操作。
使用工具(如Valgrind)检测内存泄漏。
指针是一个变量,其存储的是另一个变量的内存地址。通过指针,可以间接访问和操作内存中的数据。
传值:函数接收参数的副本。在函数内部对参数所做的任何修改都不会影响原始数据。
传址:函数接收指向实际参数的指针或引用。在函数内部对参数所做的修改会反映到原始数据上。
new/delete:是C++操作符,用于动态内存分配和释放。new分配内存并调用构造函数(如果是对象),delete释放内存并调用析构函数(如果是对象)。
malloc/free:是C语言标准库函数,仅用于分配和释放内存,不调用构造函数或析构函数。
栈(Stack):用于存储局部变量和函数调用信息。分配和释放速度快,但空间有限。
堆(Heap):动态存储期,使用new或malloc等函数分配的内存,容量大,分配和释放速度较慢。堆内存需要程序员手动管理(使用delete或free)。
全局/静态存储区:存储全局变量、静态变量等,在程序运行期间一直存在。
常量存储区:存储常量数据。
代码区:存储程序代码。
C++自C++11以来,引入了许多重要的新特性,以下是一些主要的特性:
C++11:
auto 和 decltype:用于自动类型推导。
范围for:简化对容器的遍历。
移动语义和右值引用:减少数据拷贝,提高性能。
lambda 表达式:支持匿名函数对象。
智能指针(如std::unique_ptr和std::shared_ptr):自动管理内存。
并发支持:包括thread、mutex、condition_variable等。
C++14:
泛型 lambda:支持在 lambda 表达式中使用模板参数。
变量模板:允许模板不仅是类或函数,还可以是变量。
C++17:
结构化绑定:允许直接对元组或结构体进行解构赋值。
if constexpr:在编译时进行条件判断。
std::string_view:提供对字符串的高效只读访问。
std::optional 和 std::variant:提供更灵活的变量表示。
C++20:
模块:支持将代码组织成独立的模块,提高编译速度和封装性。
概念:允许对模板参数进行约束。
协程:支持协程的编写,简化异步编程。
三向比较操作符(spaceship operator,<=>):自动推导比较操作符。
C++23:
多维下标运算符:支持多维数组的直接下标访问。
显式对象形参:允许在成员函数中使用推导的this指针。
智能指针是C++11引入的,用于自动管理内存的对象,防止内存泄漏。常见的智能指针包括:
std::unique_ptr:
独占所有权的智能指针,同一时间只有一个std::unique_ptr可以指向特定对象。
优点:安全,防止多个指针指向同一内存。
缺点:不支持拷贝构造和赋值,只能通过std::move转移所有权。
std::shared_ptr:
允许多个智能指针共享对同一对象的所有权。
优点:支持拷贝和赋值,自动处理生命周期。
缺点:可能导致循环引用,需要通过std::weak_ptr解决。
std::weak_ptr:
不控制对象生命周期,只提供对std::shared_ptr的弱引用。
优点:解决std::shared_ptr的循环引用问题。
缺点:不能直接访问对象,需要通过lock方法获取std::shared_ptr。
auto 关键字用于自动类型推导,在声明变量时根据初始化表达式的类型自动为变量选择合适的类型。使用auto可以简化代码,提高可读性,特别是在处理复杂类型时。
是否全部用auto声明变量:
优点:简化代码,避免冗长的类型名称。
缺点:在某些情况下可能降低代码的可读性,特别是当自动推导的类型与预期不符时。因此,建议仅在类型冗长复杂或变量使用范围专一时使用auto。
Lambda表达式是C++11引入的一种匿名函数对象,可以捕获外部变量并在函数体内使用。Lambda表达式的基本语法为[捕获列表] (参数列表) -> 返回类型 { 函数体 }。
用法示例:
auto lambda = [](int x, int y) -> int { return x + y; };
std::cout << lambda(1, 2) << std::endl; // 输出 3
override 关键字在C++11中引入,用于明确表示派生类中的成员函数覆盖了基类中的虚函数。使用override可以帮助编译器检查是否正确地覆盖了基类中的虚函数,从而提高代码的安全性和可维护性。
是否必须:不是必须的,但强烈建议使用,因为它可以增加代码的可读性和可维护性,并帮助编译器在编译时检查潜在的错误。
右值引用是C++11中引入的一种新特性,用于表示即将被销毁的临时对象或不可修改的值。右值引用通过类型后加&&表示,例如int&&。
应用场景:
移动语义:通过右值引用和移动构造函数/移动赋值操作符,可以实现资源的高效转移,避免不必要的拷贝。
完美转发:结合模板和引用折叠规则,可以实现参数的完美转发,保持参数的左值/右值属性不变。
实际应用:
在实现容器类、智能指针等需要管理资源的类时,可以使用右值引用来优化资源转移。
在编写模板代码时,可以使用右值引用来实现完美转发,提高代码的通用性和灵活性。
应用:
优点:
std::forward
可以在模板中实现参数的完美转发,从而保持传入参数的值类别。定义:Lambda表达式是一种能够定义匿名函数对象的方式,可以用来捕获外部变量并形成闭包。
基本语法:
[capture](parameters) -> return_type {
// function body
}
auto add = [](int a, int b) { return a + b; };
用途:
std::sort
、std::for_each
等。虚函数:允许子类重写父类的方法,实现多态。只有当基类中定义了虚函数,派生类才能重写该函数。
虚表:每个含有虚函数的类都有一个虚表(vtable),其中存储着指向该类的虚函数的指针。每个类的实例有一个指向该虚表的指针(即虚指针 vptr),用于在运行时确定调用哪个函数。
传值:
传址:
深复制:
浅复制:
以下是有关C++的一些面试问题的详细回答:
前置++ (++i
):
i
的值增加1,然后返回 i
的新值。后置++ (i++
):
i
的当前值,然后将 i
的值增加1。i
的值。总结:
map:
set:
其他容器:
内存空间划分:
new
和delete
管理。值类型和引用类型:
C#的垃圾回收原理:
new:
MyClass* obj = new MyClass();
delete:
delete obj;
malloc:
void* ptr = malloc(sizeof(MyClass));
free:
free(ptr);
区别总结:
new
和delete
是C++运算符,可以自动调用构造/析构函数;而malloc
和free
是C标准库函数,仅负责内存分配和释放,不处理对象生命周期。造成原因:
delete
或free
,造成程序占用内存逐渐增大。避免方法:
std::unique_ptr
和std::shared_ptr
)自动管理资源的生命周期,确保在超出作用域时自动释放内存。以下是针对C++面试问题的详细回答:
C++11引入了几种智能指针,主要包括:
std::unique_ptr
:
unique_ptr
拥有,不能被复制,可以通过std::move
进行转移。std::shared_ptr
:
shared_ptr
共享同一块内存。使用引用计数管理资源的生命周期,当最后一个shared_ptr
被销毁时,内存才会释放。std::weak_ptr
:
shared_ptr
配合使用,不增加引用计数,防止循环引用的问题。可以从一个shared_ptr
获得,其目的在于观察而不控制对象的生命周期。静态链接库(Static Library):
动态链接库(Dynamic Link Library, DLL):
关于EXE与链接:
.exe
文件是静态链接的,而.dll
文件是动态链接的。这意味着EXE在编译时已将所需的库代码集成,但在运行时可能仍然可以调用动态库。int **p
可以用来表示一个二维数组或数组的数组。int rows = 5;
int cols = 4;
int **array = new int*[rows]; // 创建一维指针数组,用于指向每行
for (int i = 0; i < rows; i++) {
array[i] = new int[cols]; // 为每行分配列数
}
// 使用完后记得释放内存
for (int i = 0; i < rows; i++) {
delete[] array[i];
}
delete[] array;
定义方式:
*
符号定义,如int *ptr;
&
符号定义,如int &ref = var;
空值:
nullptr
或指向某个有效地址。重新赋值:
语法:
*
,如*ptr
。#define
:
#define PI 3.14
或 #define SQUARE(x) ((x)*(x))
#ifdef
/ #ifndef
:
#ifdef DEBUG
std::cout << "Debug mode" << std::endl;
#endif
#if
/ #else
/ #elif
:
#include
:
#undef
:
#undef PI
取消对 PI
的定义。#pragma
:
#pragma once // 防止头文件被多次包含
#line
:
#error
:
#ifdef DEBUG
#error "Debug mode is enabled!"
#endif
null
:
null
是一个常量,通常用于表示空指针。在 C++ 中,null
实际上是一个宏,代表整数常量 0。使用 null
时可能会引发类型不明确的问题,因为它可以隐式转换为任何指针类型。nullptr
:
null
不同的是,nullptr
是一个类型安全的指针常量,不能隐式转换为整数或其他类型。这使得在函数重载中更清晰,加上 nullptr
使得代码在处理空指针时更加安全。int* p1 = nullptr; // 正确
int* p2 = null; // 在C++标准中,推荐使用nullptr,null可能会导致警告