八股文C++篇(超级全)

文章目录

const

说说const int *a, int const *a, const int a, int *const a, const int *const a分别是什么,有什么特点。

  • const int *a==int const *a:可以通过 a 访问整数值,但不能通过 a 修改该整数的值,指针本身是可变的,可以指向不同的整数
  • const int a:a变量变成常量,不可修改
  • int *const a:a的值可以更改,但是指向它的指针不能更改
  • int const *const a:a本身和指向它的指针都不能更改

这里其实最重要的是第一点和第三点的区别,为了记忆方便,建议只记int * const a这种,只要在印象中不是这种写法,那么一定是第一种写法

const成员函数

  • 常函数内不能修改成员变量
  • 对于类的成员函数,有时候必须指定其返回值为const类型,以使得其返回值不为“左值”。

我们来看一下一个简单的示例代码,这里使用成员初始化列表为a赋初值,可以发现在常函数f中直接修改a会提示编译失败,这时我们把a修改成const int 类型,编译成功,但是仅仅是编译成功,下面输出依旧输出10

#include <iostream>

class A {
public:
    int a;
    A() :a(10) {}
    void f() const{
        a = 10;//编译失败:提示表达式必须是可修改的左值
    const int a = 15;
    }
}

int main() {
    A a;
    a.f();
    std::cout << a.a;//输出10
    return 0;
}

但是凡事都有例外,如果该成员变量被mutable关键字修饰,那么就可以在常函数中修改

#include <iostream>

class A {
public:
    mutable int a;
    A() :a(10) {}
    void f() const{
       // a = 10;
     a = 15;//编译成功
    }
};

int main() {
    A a;
    a.f();
    std::cout << a.a;//输出15
    return 0;
}

const和#define的区别

(1) const 常量有数据类型,而宏常量没有数据类型。编译器可以对前者进行类型安全检查。
而对后者只进行字符替换,没有类型安全检查,并且在字符替换可能会产生意料不到的错误(边际效应) 。
(2)有些集成化的调试工具可以对 const 常量进行调试,但是不能对宏常量进行调试。
(3)#define是在编译的预处理阶段起作用,而const是在编译、运行的时候起作用。
(4)#define定义的常量不分配内存,而const定义的常量会分配在常量存储区中。

什么const在常量区,什么const在栈区,什么const放入符号表优化

  • 如果const修饰的是全局变量放到常量区
  • 如果const修饰的是局部变量放在栈区
  • 如果const修饰的变量没有被使用则会放到符号表中,其内容不会分配空间

虚函数

作用

父类型的指针指向其子类的实例,然后通过父类的指针调用实际子类的成员函数。

实现

每个虚函数都会有一个与之对应的虚函数表,该虚函数表的实质是一个指针数组,存放的是每一个对象的虚函数入口地址。对于一个派生类来说,他会继承基类的虚函数表同时增加自己的虚函数入口地址,如果派生类重写了基类的虚函数的话,那么继承过来的虚函数入口地址将被派生类的重写虚函数入口地址替代。那么在程序运行时会发生动态绑定,将父类指针绑定到实例化的对象实现多态。每个类只有一个虚函数表,虚函数表是在编译的时候就确定的了。

纯虚函数

用户不能创建基类的实例,只能创建派生类的实例

虚函数在什么时候调用?

虚函数在运行时根据实际对象的类型来确定调用哪个函数,而不是根据指针或引用的类型来确定。当一个虚函数被定义为类的成员函数时,它会被标记为虚函数。在调用虚函数时,程序会查找该函数的实际类型,并在运行时调用该类型的实现。这就允许程序在运行时动态地选择执行哪个版本的虚函数,从而实现多态性。虚函数通常与基类指针或引用一起使用,可以实现基类指针或引用调用派生类的函数。

大小

虚函数表是一个存储虚函数指针的数组,每个类有一个虚函数表,每个对象有一个指向虚函数表的指针。虚函数表的大小取决于类中有多少个虚函数,而对象中的虚函数表指针的大小取决于编译器和操作系统2。一般来说,在32位系统下,指针占4个字节,在64位系统下,指针占8个字节。

C++ 中哪些函数不能被声明为虚函数?

构造函数:构造函数不能被声明为虚函数。因为构造函数是用来创建对象的,而虚函数是根据对象的类型来动态调用的。如果构造函数是虚函数,那么在创建对象时就无法确定调用哪个版本的构造函数,会导致逻辑错误
友元函数:友元函数实际上并不属于类的成员函数,所以不能被定义为虚函数
普通函数:普通函数只能被重载,不能被重写

为什么虚函数不能是模板函数?

因为模板函数在编译时会被实例化为多个不同的函数,而虚函数需要在运行时才能确定调用哪个函数。在C++中,虚函数的实现依赖于虚函数表(vtable)和虚函数指针(vptr),而这些在编译时就需要确定下来。因此,虚函数不能是模板函数。

虚函数表既然希望类的所有对象共享为什么不放在全局区

虚函数表不能放在全局区,因为全局区是存放全局变量和静态变量的,而虚函数表不是变量,而是一组指向类成员函数的指针。如果放在全局区,会导致内存浪费和混乱。
混乱:虚函数表是在编译期就确定了大小和内容的,而全局区是在运行期才分配空间的。如果把虚函数表放在全局区,就需要在运行期动态地为每个类分配空间,并且要保证不同类之间不会发生冲突。这样就增加了程序的复杂度和出错的可能性

菱形继承

菱形继承(Diamond Inheritance)是一种多重继承的情况,其中一个派生类同时继承自两个直接或间接共同基类,而这两个基类又继承自同一个共同的基类。这样就形成了一种菱形的继承结构,因此称为"菱形继承"。
解决方法:将两个父类添加成virtual,相当于爷爷直接拿出指针给孙子

类型转换

static_cast

static_cast 用于执行非多态类型之间的类型转换,例如整型和浮点型之间的转换、基类和派生类之间的指针或引用转换、void 指针和其他指针类型之间的转换等。该转换在编译时完成,通常不会检查运行时错误。

dynamic_cast

dynamic_cast 用于在运行时进行多态类型的转换。它通常用于将基类指针或引用转换为派生类指针或引用,以及在类层次结构中进行下行转换

dynamic_cast和虚函数的区别

dynamic_cast和虚函数的区别

reinterpret_cast

reinterpret_cast 用于在不同的指针类型之间进行转换,**例如将一个指针转换为一个整数,或将一个整数转换为一个指针。**该转换通常不进行类型检查,因此潜在地不安全,只应在极少数特殊情况下使用。

const_cast

const_cast 用于在去除变量的 const 修饰符或 volatile 修饰符时使用。它可以将指向常量对象的指针或引用转换为指向非常量对象的指针或引用,或者将指向非常量对象的指针或引用转换为指向常量对象的指针或引用。

volatile关键字

在 C++ 中,关键字 volatile 用于声明一个变量是易变的(volatile variable),即该变量可能会在程序中的任意时刻被意外地改变。这意味着,当读取一个易变的变量时,编译器不会从缓存中读取该变量的值,而是每次都会从内存中重新读取该变量的值。同样地,当写入一个易变的变量时,编译器也不会将该变量的值存储在缓存中,而是立即将该变量的值写入内存中。

构造函数一大家子

拷贝构造函数

什么时候调用拷贝构造函数?

  • 用已经初始化的对象给另一个初始化的对象赋值
  • 函数用对象作为返回值
  • 函数用对象作为参数

析构函数

为什么要用虚的?

A* p = new B;
delete p;(A是父类 B是子类)
如果不定义虚析构 那么删除P只调用A的析构
定义为虚析构之后,删除P就会调用AB的析构

移动构造函数

在C++中,当一个对象被复制时,其内部资源通常会被复制,这可能会导致性能问题,特别是在处理大型数据结构时。

移动语义是指,当一个对象被移动而不是复制时,其内部资源可以被“窃取”,而不需要进行复制操作。移动操作比复制操作更高效,因为它不需要分配新的内存或复制现有的内存。

C++类内是否可以定义引用?

可以,但是必须使用成员初始化列表为引用变量初始化,构造函数的形参也必须是引用类型

模板类

模板类是什么时候实现的

模板类的实现不是在程序运行时期间动态生成的,而是在编译阶段根据需要进行实例化和生成对应的代码。这也是为什么在使用模板类时,模板类的声明和定义通常需要放在头文件中,以便编译器在需要的地方进行实例化,并生成相应的代码。

模板实例化

显式是程序员指定什么样的类型实现什么样的函数,后者是编译器来决定什么类型

template class MyTemplate<int>;  // 显式实例化一个存储整数的 MyTemplate 类
MyTemplate<int> intObj;  // 隐式:编译器会根据这个使用自动实例化一个存储整数的 MyTemplate 类

模板具体化

当模板使用某种类型类型实例化后生成的类或函数不能满足需要时,可以考虑对模板进行具体化。具体化时可以修改原模板的定义,当使用该类型时,按照具体化后的定义实现,具体化相当于对某种类型进行特殊处理。

模板为了解决什么问题?

简单来讲 就是解决多态问题

模板的声明和定义为什么不能分开写,要想分开写该怎么做

模板的声明和定义不能分开写的原因是,编译器在编译模板时需要知道模板的完整定义,才能根据具体的类型参数生成相应的代码。如果只有声明而没有定义,编译器就无法生成正确的代码,链接时就会出现未定义引用的错误。

法一:一种是在声明文件中包含定义文件

// template.h
template <typename T>
void swap(T& a, T& b);

#include "template.cpp"
// template.cpp
template <typename T>
void swap(T& a, T& b) {
  T temp = a;
  a = b;
  b = temp;
}

这样做的好处是可以保持声明和定义的分离,但缺点是每次修改定义文件都需要重新编译所有包含声明文件的源文件。
法二:在定义文件中显式实例化需要用到的类型参数

// template.h
template <typename T>
void swap(T& a, T& b);
// template.cpp
template <typename T>
void swap(T& a, T& b) {
  T temp = a;
  a = b;
  b = temp;
}

// explicit instantiation for int and double types
template void swap<int>(int&, int&);
template void swap<double>(double&, double&);

这样做的好处是可以避免重复编译和代码膨胀,但缺点是需要提前知道所有可能用到的类型参数,并且每增加一个类型参数都需要修改定义文件

模板特化

模板特化是指为某些特定类型提供不同于通用模板的具体实现

全特化

全特化是指为所有的模板参数都指定具体类型或值的情况

偏特化

偏特化是指只为部分的模板参数指定具体类型或值,或者限制参数的范围。偏特化只适用于类模板,不适用于函数模板

模板在编译时生成的代码是否会相同,生成的相同的代码如何处理

一种情况是,如果模板的定义和声明都放在头文件中,并且在多个源文件中被包含和使用,那么编译器会为每个源文件生成相同的模板实例化代码。这样会导致目标文件中存在重复的代码段,增加了目标文件的大小,并且可能引起链接错误。为了避免这种情况,可以使用 extern 关键字来声明一个外部模板,在一个源文件中显式地实例化该模板,并且在其他源文件中只引用该实例化。
另一种情况是,如果模板的定义放在一个源文件中,并且在其他源文件中被包含和使用,那么编译器会根据不同的类型参数生成不同的模板实例化代码。这样可以避免重复的代码段,但是也可能导致链接错误,因为其他源文件无法找到该源文件中定义的模板。为了避免这种情况,可以将模板的声明放在头文件中,并且在需要使用该模板的源文件中包含该头文件

C++ 类对象的初始化顺序

父类构造函数–>成员类对象构造函数–>自身构造函数
(当一个类的成员是另一个类的对象时,这个对象就叫成员对象.)

STL

容器

顺序型容器

vector

特点:

  • 顺序序列
  • 动态数组
  • 尾删有较佳性能
第二个模板形参?

vector 的第二个模板形参是分配器(allocator),用于分配和管理 vector 内部存储元素的内存。分配器可以控制内存分配的策略,例如内存池等。如果不指定分配器,默认使用 std::allocator。

分配器通常是一个模板类,提供了 allocate 和 deallocate 等成员函数来分配和释放内存。在 vector 内部,使用分配器来分配和释放存储元素的内存,可以方便地替换默认的内存分配器,实现自定义的内存分配策略。

vector调用resize的时候,如果是元素是一个类,会不会调用这些函数的析构函数?

如果调用resize函数使得vector的大小变小了,那么后面的元素会被析构掉,也就是会调用元素类的析构函数。如果调用resize函数使得vector的大小变大了,那么新添加的元素会调用元素类的默认构造函数进行初始化,而不会调用析构函数。

使用Vector需要注意什么?
  • 为避免频繁的扩容操作,可以使用 reserve() 方法在插入元素之前预留一定的空间,以提高性能
  • 在使用 vector 进行大量元素操作时,可以使用 emplace_back() 方法而不是 push_back() 方法,以避免元素拷贝的开销
  • 在需要删除元素时,可以使用 erase() 方法进行删除。但是,需要注意的是,如果要删除多个元素,应该首先对要删除的元素进行排序,并使用 erase() 方法一次性删除,以避免多次扩容操作
如果扩容时会引发自定义类型挨个复制构造,C++有什么机制来避免这一点

在进行 vector 扩容时,如果存储的是自定义类型,会挨个复制构造元素,可能会造成性能问题。为了避免这一点,可以使用移动语义来优化。

在 C++11 引入的移动语义中,我们可以通过 std::move() 函数将一个对象转化为右值引用,这样就可以在元素的拷贝构造函数中实现移动语义,将对象的资源所有权从一个对象转移到另一个对象中,而不是进行深拷贝。

deque

特性:

  • 双向队列
  • 在两端增删元素有较佳性能
list

特性:

  • 双向链表
  • 不支持随机存取

关联式容器

set

特性:

  • 不允许相同元素
  • 自动排序
  • 原理:红黑树
map
  • first和second,并且根据first排序
  • 实现原理:红黑树
  • map不允许容器中有重复的key值元素
红黑树的性质,各种操作时间复杂度

自动排序,稳定
查找,插入,删除都是O(logn)

unordered_map

umap底层是哈希表

哈希表跟红黑树的比较,优缺点、适用场合,各种操作的时空复杂度

哈希表适合小数据,查找插入删除最好都是O(1),最坏O(n),缺点是容易发生哈希冲突,设计哈希函数也比较困难
红黑树适合大数据集,但是代码实现较为复杂

空间配置器

定义

在C++ STL中,空间配置器便是用来实现内存空间(一般是内存,也可以是硬盘等空间)分配的工具,他与容器联系紧密,每一种容器的空间分配都是通过空间分配器alloctor实现的。

背景

开辟内存一般分为两步,一步是用构造函数,一部分用malloc或者new,前者直接在函数调用栈开辟空间,而后者先在堆里开辟空间,再隐式调用构造函数

实现

关于内存空间的配置与释放,SGI STL采用了两级配置器:一级配置器主要是考虑大块内存空间,利用malloc和free实现;二级配置器主要是考虑小块内存空间而设计的(为了最大化解决内存碎片问题,进而提升效率),采用链表free_list来维护内存池(memory pool),free_list通过union结构实现,空闲的内存块互相挂接在一块,内存块一旦被使用,则被从链表中剔除,易于维护。

迭代器

迭代器用过吗?什么时候会失效?

顺序容器使用删除会使后面的迭代器失效(自动往前进一,导致地址全变,所以会失效),解决办法:it=earse(it),即返回删除元素下一个的迭代器
关联容器map由于内部是红黑树,使用erase不会失效,但是需要记录一下下一个元素的迭代器,list使用上面两种方法都行

迭代器的作用

和指针的区别:
迭代器不是指针,是类模板,表现的像指针。他只是模拟了指针的一些功能,重载了指针的一些操作符,–>、++、–等。迭代器封装了指针,是一个”可遍历STL( Standard Template Library)容器内全部或部分元素”的对象,本质是封装了原生指针,是指针概念的一种提升,提供了比指针更高级的行为,相当于一种智能指针,他可以根据不同类型的数据结构来实现不同的++,–等操作。

迭代器返回的是对象引用而不是对象的值,所以cout只能输出迭代器使用取值后的值而不能直接输出其自身

迭代器相较于指针

迭代器相对于指针的优点在于,它提供了一些安全性和抽象性的保证。例如,如果你使用一个指向数组元素的指针,你可以对它进行任何操作,包括越界访问和非法修改等操作,这可能会导致内存错误和程序崩溃。而如果你使用一个vector迭代器,则可以避免这些问题,因为迭代器会自动检查越界和非法操作,并在出错时抛出异常或者进行其他处理。

说说 STL 中 resize 和 reserve 的区别

介绍概念:capacity:该值在容器初始化时赋值,指的是容器能够容纳的最大的元素的个数。还不能通过下标等访问,因为此时容器中还没有创建任何对象。
size指的是此时容器中实际的元素个数。可以通过下标访问0-(size-1)范围内的对象。

resize:

resize即修改capacity大小,也修改size大小

reserve:

reserve只修改capcaity大小

resize既分配了空间,也创建了对象;reserve表示容器预留空间,但并不是真正的创建对象,需要通过insert()或push_back()等创建对象。

STL 容器动态链接可能产生的问题

给动态库函数传递容器的对象本身,则会出现内存堆栈破坏的问题。
产生问题的原因,容器和动态链接库相互支持不够好,动态链接库函数中使用容器时,参数中只能传递容器的引用,并且要保证容器的大小不能超出初始大小,否则导致容器自动重新分配,就会出现内存堆栈破坏问题。

push_back 和 emplace_back 的区别

如果要将一个临时变量push到容器的末尾,push_back()需要先构造临时对象,再将这个对象拷贝到容器的末尾,而emplace_back()则直接在容器的末尾构造对象,这样就省去了拷贝的过程。

STL 中 vector 与 list 具体是怎么实现的?常见操作的时间复杂度是多少?

vector:开辟三倍内存,旧数据开辟到新内存,释放旧的内存,指向新内存

新特性

智能指针

share_ptr

std::shared_ptr是一种共享式智能指针,它可以让多个shared_ptr实例同时拥有同一个内存资源。shared_ptr内部维护了一个计数器,记录当前有多少个shared_ptr实例共享同一块内存。只有当计数器变为0时,才会自动释放内存。因此,shared_ptr可以避免多个指针指向同一块内存时出现的内存泄漏和悬空指针等问题。

unique_ptr

std::unique_ptr是一种独占式智能指针,它可以保证指向的内存只被一个unique_ptr实例所拥有。当unique_ptr被销毁时,它所拥有的内存也会被自动释放。unique_ptr还支持移动语义,因此可以通过std::move来转移拥有权。

使用release()方法来移交指向的对象

weak_ptr

用来解决shared_prt相互引用冲突的结果
举个一个不太恰当的例子,A和B相互加了微信,假设我们用一个指针来指向自己的微信朋友,如果是shared_ptr,那么A和B的生命周期是相互影响的,而实际上我们并不希望这种强绑定,比如假设B注销了账户,A根本不用知道,只有当A想发消息给B的时候系统才会发出提示:您还不是该用户的朋友。这时候weak_ptr就派上用场了。这也就是weak_ptr的第一种使用场景:
当你想使用对象,但是并不想管理对象,并且在需要使用对象时可以判断对象是否还存在

  • 解决循环引用:当两个或多个对象相互持有对方的 shared_ptr,会形成循环引用,导致对象无法释放。通过将其中一个对象的指针设置为 weak_ptr,而不是 shared_ptr,可以打破循环引用,避免内存泄漏。
  • 安全地访问对象:在需要访问被 shared_ptr 管理的对象时,可以通过 weak_ptr 的 lock() 方法尝试转换为 shared_ptr,如果对象仍然存在,则返回一个有效的 shared_ptr,否则返回一个空指针。
  • 提高性能:weak_ptr 不会增加对象的引用计数,因此不会影响对象的生命周期,也不会阻止对象的销毁。这样可以更灵活地管理对象的生命周期,提高程序性能。
怎么知道weak_ptr失效了没

可以通过expired()函数来判断一个weak_ptr是否已经失效,如果expired()返回true,则表示它指向的对象已经被销毁或释放了。另外,使用lock()函数获取weak_ptr指向的对象时,如果返回的是一个空的shared_ptr,也可以判断weak_ptr是否已经失效。

lambda表达式

lambda语法:

[capture] (parameters) mutable ->return-type{statement}

[capture]含义
  • []。没有任何函数对象参数。
  • [=]。函数体内可以使用 Lambda 所在范围内所有可见的局部变量(包括 Lambda 所在类的
    this),并且是值传递方式(相当于编译器自动为我们按值传递了所有局部变量)。
  • [&]。函数体内可以使用 Lambda 所在范围内所有可见的局部变量(包括 Lambda 所在类的
    this),并且是引用传递方式(相当于是编译器自动为我们按引用传递了所有局部变量)。
  • [this]。函数体内可以使用 Lambda 所在类中的成员变量。
  • [a]。将 a 按值进行传递。按值进行传递时,函数体内不能修改传递进来的 a 的拷贝,因为默认情况下函数是 const
    的,要修改传递进来的拷贝,可以添加 mutable 修饰符。
  • [&a]。将 a 按引用进行传递。

中括号 “[]” 表示Lambda表达式的捕获列表,用于指定Lambda表达式访问外部作用域中的变量的方式。捕获列表可以为空,或者包含一个或多个捕获项,

int a = 1;
auto lambda = [a](int x, int y) -> int { return a + x + y; };

在这个例子中,捕获列表包含一个捕获项 “a”,表示Lambda表达式将访问外部作用域中的变量 “a”。

(parameters)含义

标识重载的 () 操作符的参数,没有参数时,这部分可以省略。参数可以通过按值(如: (a, b))和按引用 (如: (&a, &b))
两种方式进行传递。

[](int x, int y) -> int { return x + y; }

圆括号 “()” 表示Lambda表达式的参数列表,可以包含零个或多个参数。在这个例子中,Lambda表达式有两个参数,分别是一个整数 “x” 和一个整数 “y”

mutable 或 exception 声明

这部分可以省略。按值传递函数对象参数时,加上 mutable
修饰符后,可以修改传递进来的拷贝(注意是能修改拷贝,而不是值本身)。exception
声明用于指定函数抛出的异常,如抛出整数类型的异常,可以使用 throw(int)。

->return-type->

返回值类型:标识函数返回值的类型,当返回值为 void,或者函数体中只有一处 return
的地方(此时编译器可以自动推断出返回值类型)时,这部分可以省略

{statement}{函数体}

标识函数的实现,这部分不能省略,但函数体可以为空。

Lambda表达式如何对应到函数对象

当定义一个Lambda表达式时,编译器会生成一个与Lambda表达式对应的新的(未命名的)函数对象类型和该类型的一个对象。这个函数对象可以重载函数调用运算符(),从而具有类似函数的行为。

圆括号传参数是如何实现的

圆括号传参数是通过函数调用运算符()来实现的。

当你使用圆括号传递参数给一个lambda表达式时,实际上是调用了它生成的函数对象的函数调用运算符(),并将参数传递给它。

函数调用运算符()会根据lambda表达式的定义来执行相应的代码,并返回一个值(如果有的话)。

所以,你可以把圆括号传参数看作是一种调用函数对象的方式,它让你不需要知道函数对象的名字或者类型就可以使用它。

方括号捕获外部变量(闭包)是如何实现的

方括号捕获外部变量(闭包)是通过将外部变量作为函数对象的成员来实现的。

当你在方括号中指定一个外部变量时,编译器会为你生成一个函数对象类型,它包含了这个外部变量作为它的一个成员。

当你创建一个函数对象时,这个成员会被初始化为外部变量的值或者引用,这取决于你是用=还是&来捕获它。

当你调用函数对象时,这个成员就可以在lambda表达式中使用,就像一个普通的局部变量一样。

所以,你可以把方括号捕获外部变量看作是一种创建闭包的语法糖,它让你不需要显式地定义一个类或者接口来保存外部变量的状态。

  • 值捕获(capture by value):使用 “=”
    将外部变量按值进行捕获。Lambda表达式会在创建时将外部变量的值复制一份到闭包中
  • 引用捕获(capture by reference):使用 “&”
    将外部变量按引用进行捕获。Lambda表达式会在创建时绑定到外部变量的内存地址,以便在Lambda表达式中修改变量的值
  • 隐式捕获:使用 “[]” 作为空方括号,表示隐式捕获所有在Lambda表达式中使用的外部变量。在Lambda表达式中使用的变量会被自动按值进行捕获。
 int x = 3;
auto lambda = [&] { return x * x; };
int result = lambda();  // result = 9

需要注意的是,对于值捕获和隐式捕获,Lambda表达式在创建时会复制一份外部变量的值到闭包中,如果在Lambda表达式中修改闭包中的变量值,不会影响外部变量的值。而对于引用捕获,Lambda表达式会直接操作外部变量,可以改变其值。

右值引用

右值引用是C++11引入的一种引用类型,它用于表示临时对象和即将销毁的对象
在了解右值之前,我们先来了解一下左值

左值的概念

左值是指可以出现在赋值运算符左边的表达式,也就是具有内存地址且可被取地址的表达式。通常来说,变量、对象以及可以引用的表达式都是左值。左值表示的是一个具体的内存位置,可以对其进行读取和写入操作

int x = 10; // x 是一个左值
int y = 15;
int &ref = y; // ref 是一个左值

看完了左值之后我们来看右值

右值的概念

右值是指不能出现在赋值运算符左边的表达式,通常是临时性的、不具有明确内存地址的值。字面常量、临时对象、函数返回值等都属于右值。右值表示的是一个临时的值,不能被取地址。

int x=10; //在刚才的例子中,x是左值,而10就是右值
std::string greeting = std::string("Hello, World!"); // std::string("Hello, World!") 是一个右值
int val = getValue(); // getValue() 的返回值是一个右值

右值引用的使用

和刚才的套路一样,为了研究右值引用是什么,我们先来研究左值引用

左值引用的概念

左值引用是 C++ 中最常见的引用类型,用于绑定到左值表达式上。左值引用通过 & 符号表示。

可以说左值引用就是我们平时常用的“引用”

void processObject(Object& obj) {
    // 对传入的对象进行处理
}

Object largeObject;
processObject(largeObject);
//用于传递可修改的参数;用于避免拷贝开销
右值引用的概念

右值引用是 C++11 引入的一个重要特性,用于绑定到临时对象或右值表达式上,以支持移动语义和完美转发。右值引用通过 && 符号表示。

int num = 10;
//int && a = num;  //右值引用不能初始化为左值
int && a = 10;

和常量左值引用不同的是,右值引用还可以对右值进行修改。例如:

  int&& a = 10;
  a = 100;
  std::cout << a << std::endl;//输出100

右值引用的具体案例包括移动语义和完美转发

移动语义

移动语义是 C++11引入的重要特性,旨在提高资源管理的效率和性能。在传统的拷贝操作中,对象的资源是通过复制(拷贝构造函数)的方式从一个对象传递到另一个对象,这会涉及到深拷贝操作,即将资源完全复制一份,导致了额外的开销。而移动语义则引入了右值引用和移动构造函数(移动赋值运算符),允许对象的资源在不需要进行深拷贝的情况下进行高效地转移。简单的理解,移动语义指的就是将其他对象(通常是临时对象)拥有的内存资源“移为已用”。换句话说,就是以浅拷贝的方式复制指针,然后将原指针置为空指针。移动构造函数就是通过移动语义的方式来初始化对象的。

std::vector<int> vec1 = {1, 2, 3};
std::vector<int> vec2 = std::move(vec1);
如何将左值强制转换为右值?

move函数

int x = 10;
int&& r = move(x);   //将左值强制转换为右值

移动构造函数和拷贝构造函数的区别

  1. 拷贝构造函数用于创建一个新对象,并将其初始化为已存在的对象的副本。这个过程通常涉及到内存的分配和数据的复制,因此它可能比较昂贵。
  2. 移动构造函数用于创建一个新对象,并将其初始化为另一个对象的资源所有权的转移。这个过程通常涉及到指针的复制,而不是数据的复制,因此它通常比拷贝构造函数更加高效。移动构造函数通常会使用右值引用来接受一个临时对象或者一个即将被销毁的对象,并将其资源所有权转移到新对象中。

转发和完美转发

转发是指在函数中将参数按照原始的类型和值,转发给另一个函数。

常规转发

常规转发是指将参数通过传值或引用的方式传递给另一个函数,这是C++中的传参方式。但是在传递参数时,会存在一些问题。例如,当我们想把一个右值参数传递给一个函数时,我们可能会遇到编译器错误。另一个例子是当我们要将一个右值参数转发给一个函数时,但是我们不知道该使用传值还是传引用,因为这个决定取决于被调用的函数的定义。为了解决这些问题,C++11 引入了完美转发。

//错误案例
#include <iostream>
#include <utility>

void func(int&& x) {
    std::cout << "传递右值参数:" << x << std::endl;
}

template <typename T>
void wrapper(T&& arg) {
    func(arg);  // 尝试将通用引用参数传递给接受右值引用的函数,会导致编译错误
}

int main() {
    int value = 42;
    wrapper(std::move(value));  // 将左值转换为右值传递给 wrapper
    return 0;
}

普通的引用传递无法正确地将右值参数传递给接受右值引用的函数,这种情况下需要使用完美转发来解决。

完美转发

完美转发是指将参数以原始的类型和值传递给另一个函数,并保留其右值或左值特性。这可以通过使用转发引用(forwarding reference)和 std::forward 函数来实现。转发引用是一种通用引用,它可以引用任何类型的值,并且可以保留值的右值或左值特性。当我们使用转发引用作为函数的参数时,我们可以在函数内部使用 std::forward 来将参数转发给另一个函数,以保留参数的右值或左值特性。

  1. 必须使用转发引用作为参数类型。
  2. 必须使用 std::forward 函数进行转发
  3. 被转发的参数必须是右值或左值引用。
#include <iostream>
#include <utility>

void func(int& x) {
    std::cout << "传引用:" << x << std::endl;
}

template <typename T>
void wrapper(T&& arg) {
    func(std::forward<T>(arg));  // 使用 std::forward 进行完美转发
}

int main() {
    int value = 42;
    wrapper(value);  // 正确地将左值参数传递给 wrapper,然后再传递给 func
    return 0;
}

在常规转发中,参数被传递给了被调用函数,但是它们的类型都是引用类型,因此在函数内部处理时,不会进行任何类型转换。也就是说,如果我们传递给常规转发的参数是一个左值,那么被调用函数内部处理时,它们依然是左值引用;如果我们传递的是一个右值,那么它们依然是右值引用。这样可能会导致一些效率问题,比如如果被调用函数需要将参数进行复制,那么这个过程可能会比较耗时。
而在完美转发中,我们使用了转发引用 T&&,并在函数内部使用了 std::forward 函数来进行类型转换。这样可以保留参数的原有类型特性,使得被调用函数内部处理时,参数的类型会根据传递给它的参数类型进行调整,从而避免了一些不必要的类型转换操作。此外,通过完美转发,我们还可以保留参数的右值特性,避免了一些额外的复制操作,从而提高了代码的效率。

auto关键字,lambda表达式,nullptr,成员初始化列表

static关键字

面向过程

静态全局变量:静态全局变量在声明它的整个文件中都是可见的,而在文件之外是不可见的;(作用域是整个文件)变量的生存周期存在于整个程序运行期间。

#include <iostream>
#include <stdio.h>
 
static int a = 10;
 
void Func()
{
	a++;
}
 
int main()
{
	printf("a = %d\n", a);//输出:10
	Func();//输出:10
	printf("a = %d\n", a);//输出:11
 
	system("pause");
    return 0;
}

静态局部变量:内存存放在程序的全局数据区中,静态局部变量在程序执行到该对象声明时,会被首次初始化。其后运行到该对象的声明时,不会再次初始化(只会被初始化一次),变量的生存周期存在于整个程序运行期间。

#include <iostream>
#include <stdio.h>
 
void Func()
{
	static int a = 5;
	printf("a = %d\n", a);
	a++;
}
 
int main()
{
	for (int i = 0; i < 5; i++)
	{
		Func();  //打印结果:5 6 7 8 9
	}
 
	system("pause");
    return 0;
}

无论是静态全局变量还是静态局部变量都存放在全局区

静态函数(主要目的确定作用域):作用域只在声明它的文件当中,不能被其他文件引用,其他文件可以定义同名的全局函数,其他文件想要调用本文件的静态函数,需要显示的调用extern关键字修饰其声明。

#include <iostream>
#include <stdio.h>
 
static void Func()
{
	printf("This is a static function\n");
}
 
int main()
{
	Func(); 
	
	system("pause");
    return 0;
}

面向对象

静态成员变量:用于修饰 class 的数据成员,即所谓“静态成员”。这种数据成员的生存期大于 class 的对象(实体 instance)。静态数据成员是每个 class 有一份,普通数据成员是每个 instance 有一份,因此静态数据成员也叫做类变量,而普通数据成员也叫做实例变量。
诞生比构造函数早,在类声明的时候就产生了
静态数据成员必须显式初始化分配内存,在其包含类没有任何实例化之前已经有内存分配;
静态数据成员与其他成员一样,遵从public,protected,private的访问规则;静态数据成员内存存储在全局数据区,只随着进程的消亡而消亡
静态数据成员不进入程序全局命名空间,不会与其他全局名称的同名同类型变量冲突,静态数据成员可以实现C++的封装特性,由于其遵守类的访问权限规则,所以相比全局变量更加灵活;

静态成员函数:静态成员函数不能访问非静态(包括成员函数和数据成员),但是非静态可以访问静态
在没有实例化的类对象的条件下可以调用类的静态成员函数;
静态成员函数中没有隐含的this指针,所以静态成员函数不可以操作类中的非静态成员(由于第二条可知,类的非静态成员是在类实例化后存在的,而类的成员函数可以在类没有实例化的时候调用,故不能操作类的非静态成员);

#include <iostream>
#include <stdio.h>
using namespace std;

class Test
{
public:
	Test(int a, int b, int c) :
		m_a(a),
		m_b(b),
		m_c(c)
	{
		m = a + b + c;
	}

	static void Show()
	{
		cout << "m = " << m << endl;
	}

private:
	int m_a, m_b, m_c;
	static int m;
};

int Test::m = 0; //初始化静态数据成员

int main()
{
	Test ClassA(1, 1, 1);
	ClassA.Show();
	Test ClassB(3, 3, 3);
	ClassB.Show();
	ClassA.Show();

	Test::Show(); //输出: 9,如果把前面实例化对象全部注释则会输出0

	system("pause");
	return 0;
}

初始化

对于C语言的全局和静态变量,初始化发生在任何代码执行之前,属于编译期初始化。

而C++标准规定:全局或静态对象当且仅当对象首次用到时才进行构造(静态全局和静态局部)。
然而,静态成员变量与静态局部变量和全局变量不同。它们必须在类的外部进行初始化,并且在程序开始执行之前就已经被分配内存并初始化了。

C++编译过程

  • 预编译:在预处理阶段,预处理器会处理以#开头的预处理指令,比如#include、宏定义等。预处理器会展开头文件,替换宏定义,并进行条件编译等操作,生成一个被预处理后的源文件。
  • 编译:编译阶段是将预处理后的源文件翻译成汇编代码的过程。编译器会对源代码进行词法分析、语法分析、语义分析和优化,最终生成相应的汇编代码。
  • 汇编:汇编阶段将上一步生成的汇编代码翻译成目标文件。汇编器会将汇编代码转换成机器指令,生成一个二进制目标文件(.obj 文件或 .o 文件)。
  • 链接:链接阶段是将多个目标文件和库文件合并成一个可执行文件的过程。链接器会解析目标文件之间的引用关系,填充地址空间,解决符号重定位,并将它们合并成一个可执行文件(.exe 文件或可执行程序)。

动态链接和静态链接

  • 静态:连接的时候就把需要的函数或者过程放进了可执行文件中,即使静态库删除了依旧可以运行
  • 动态:是在链接的时候没有把调用的函数代码链接进去,而是在执行的过程中,再去找要链接的函数,生成的可执行文件中没有函数代码,静态库删除就找不到函数了。

内联函数

定义

内联函数是在 C++ 中增加的一个功能,可以提高程序执行效率。如果函数是内联的,编译器在编译时,会把内联函数的实现替换到每个调用内联函数的地方,可以与宏函数作类比,但宏函数不会进行类型检查。

意义

引入内联函数主要是解决一些频繁调用的小函数消耗大量空间的问题。

通常情况下,在调用函数时,程序会将控制权从调用程序处转移到被调用函数处,在这个过程中,传递参数、寄存器操作、返回值等会消耗额外的时间和内存,如果调用的函数代码量很少,也许转移到调用函数的时间比函数执行的时间更长。

哪些不适合作为内联函数

  1. 递归调用本身的函数
  2. 包含复杂语句的函数,例如:for、while、switch 等;
  3. 函数包含静态变量(内联函数的定义和调用是在编译期进行的,而不是在运行期。编译器会将内联函数的代码直接嵌入到调用它的地方,从而避免了函数调用的开销。但是,这也意味着每次调用内联函数时,都会生成一份新的函数代码。
    如果内联函数中有静态变量,那么每次生成新的函数代码时,也会生成新的静态变量。这样就会导致多个静态变量共存于程序中,并且互相独立,无法保持一致性);

使用内联的缺点

如果使用很多内联函数,生成的二进制文件会变大;

编译的时间会增加,因为每次内联函数有修改,就需要重新编译代码。

所以,并不是所有函数都要声明为内联函数,需要视具体情况而定。

和宏的区别

宏函数是在预编译的时候把所有的宏名用宏体来替换,简单的说就是字符串替换 ;而内联函数则是在编译的时候进行代码插入,编译器会在每处调用内联函数的地方直接把内联函数的内容展开,这样可以省去函数的调用的开销,提高效率
宏定义是没有类型检查的,无论对还是错都是直接替换;而内联函数在编译的时候会进行类型检查,内联函数满足函数的性质,比如有返回值、参数列表等

程序启动的过程

  1. 加载可执行程序:操作系统根据可执行的文件信息,分配进程空间,将代码段,数据段,BSS段等映射到进程的虚拟空间中
  2. 初始化:操作系统调用C++运行库的初始化代码,进行初始化,包括初始化全局变量,构造静态对象等
  3. 调用main()函数
  4. 根据程序设计和逻辑,在运行过程中,可能需要分配动态内存、创建新的线程、进行 I/O 操作等。
  5. 退出:当 main() 函数执行完毕,或者调用 exit() 函数结束程序运行,操作系统会回收进程空间和资源,完成程序的退出过程。

多态

静态多态

编译器在编译期间完成的,编译器会根据实参类型来推断该调用哪个函数,如果有对应的函数,就调用,没有则在编译时报错。

动态多态

虚函数

动态绑定

动态绑定是指在运行时确定函数的实际调用函数。如果一个函数被声明为虚函数,那么在运行时就可以使用动态绑定,使得调用正确的实现。这种绑定通常是通过虚函数表(Virtual Table)来实现的,虚函数表是一个存储指向虚函数地址的指针数组,每个包含虚函数的类都有一个虚函数表。例如调用 ptr->speak() 时,根据指针实际指向的对象类型进行动态绑定,调用相应的 speak() 实现

多态的好处

增强程序的可扩充性,即程序需要修改或增加功能时,只需改动或增加较少的代码。简化代码,使得不同的子类对象都可以使用同一个名称的函数,而具有不同的实现。实现动态绑定,即在运行时根据对象的实际类型来调用相应的虚函数。

多态的形式

  • 虚函数多态
  • 类模板多态
  • 重载多态

杂项

C++内存分区(五)

  • 堆区
  • 栈区
  • 全局/静态区
  • 常量区
  • 程序代码区

32位整型在大小端的区别 (0x12345678)

小端:78 56 34 12(低位在低字节)
大端:12 34 56 78(低位在高字节)

内存对齐

经过内存对⻬之后,CPU 的内存访问速度⼤⼤提升。因为 CPU 把内存当成是⼀块⼀块的,块的⼤⼩可以是 2,4,8,16 个字节,因此 CPU 在读取内存的时候是⼀块⼀块进⾏读取的,块的大小称为内存读取粒度。⽐如说 CPU 要读取⼀个 4 个字节的数据到寄存器中(假设内存读取粒度是 4),如果数据是从 0 字节开始的,那么直接将 0-3 四个字节完全读取到寄存器中进⾏处理即可。
如果数据是从 1 字节开始的,就⾸先要将前 4 个字节读取到寄存器,并再次读取 4-7 个字节数据进⼊寄存器,接着把 0 字节,5,6,7 字节的数据剔除,最后合并 1,2,3,4字节的数据进⼊寄存器,所以说,当内存没有对⻬时,寄存器进⾏了很多额外的操作,⼤⼤降低了 CPU 的性能。

内存对齐的原因

平台原因:不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
性能原因:应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次就可以了。

什么时候不应该内存对齐?

什么时候不希望进行内存对齐呢?一般来说,当我们追求空间效率而不是时间效率时,我们可以选择取消或者减小内存对齐。例如,在嵌入式系统中,由于资源有限,我们可能更关心节省空间而不是提高速度。此时我们可以使用编译器提供的选项来调整或者关闭内存对齐。

内存对齐的规则

  • 对于结构体的各个成员,第一个成员位于偏移为0的位置,结构体第一个成员的偏移量(offset)为0,以后每个成员相对于结构体首地址的offset都是该成员大小与有效对齐值中较小那个的整数倍,如有需要编译器会在成员之间加上填充字节。
  • 有效对齐值是给定值#pragma pack (n)和结构体中最长数据类型长度中较小的那个,其中n是编译器提供的选项,可以是1,2,4,8,16等
  • 除了结构成员需要对齐,结构本身也需要对齐,结构的长度必须是编译器默认的对齐长度和成员中最长类型中最小的数据大小的倍数对齐。

一个空类的大小是几字节?

1

指针和引用的区别

引用和引用变量共同占一个空间,可以说,指针看的是地址,引用看的是变量本身,所以引用更加安全(不能取到引用本身的地址。如果去取引用的地址,编译器会帮你变成去所指向变量的地址。所以对引用取地址,其实取到的是所指向的值的地址)

浅拷贝和深拷贝的区别?

浅拷贝只复制指针,新旧两个东西共享同一块内存,当对象拥有动态分配的内存时,使用浅拷贝可能会导致资源泄露或内存访问错误
深拷贝会创建一个新的对象,包括内存,这意味着每个对象都有自己独立的内存副本,即使一个对象被改变,另一个也不会受影响

struct和class的区别

struct默认公有继承,class默认私有继承
struct内不能声明函数,class可以

导入C函数的关键字是什么,C++编译时和C有什么不同?

  • C++中,导入C函数的关键字是extern,表达形式为extern “C”, extern "C"的主要作用就是为了能够正确实现C++代码调用其他C语言代码。加上extern "C"后,会指示编译器这部分代码按C语言的进行编译,而不是C++的。
  • 由于C++支持函数重载,因此编译器编译函数的过程中会将函数的参数类型也加到编译后的代码中,而不仅仅是函数名;而C语言并不支持函数重载,因此编译C语言代码的函数时不会带上函数的参数类型,一般只包括函数名。

函数指针

函数A();
函数B();
Bfuc(A);
B就是回调

意义:因为传进来的函数是不确定的。可以传函数1,也可以传函数2,直接在函数体里面调用就写死了,可以用这种方法实现多态

new和malloc

  • new是操作符 malloc是函数
  • new在调用的时候先分配内存,再调用构造函数,释放的时候先调用析构,后释放内存;而malloc没有构造函数和析构函数。
  • new发生错误抛出异常,malloc返回null
  • new返回具体类型指针,malloc需要强制转换

delete如何知道该释放多大的空间,这些信息存在什么位置

  • 一种是在分配内存时,在内存首地址之前存储一个额外的值,表示数组的大小或者元素个数。这样,在释放内存时,就可以根据这个值来确定要释放多少空间。
  • 另一种是在编译时,编译器会记录数组类型和大小的信息,并在生成代码时,将这些信息传递给delete[]操作符。这样,在运行时,delete[]就可以根据类型和大小来调用相应的析构函数和free函数

delete[]和delete的区别,基本数据类型的数组使用delete可以释放完全吗

当new申请的是C++对象数组时,delete和delete []差别就很大了,delete只会析构一个对象
delete和delete[]的区别主要在于是否调用析构函数。如果用delete[],则在回收空间之前所有对象都会首先调用自己的析构函数。基本类型的对象没有析构函数,所以回收基本类型组成的数组空间用delete和delete[]都是应该可以的;但是对于类对象数组,只能用delete[]。否则可能会造成内存泄漏或者其他错误。

存储

  1. 内存首地址
  2. 还有一种是在内存中保存一个数据结构,例如哈希表或者链表,其中记录了每个分配的内存块的地址和大小。这样,在释放内存时,就可以通过查找数据结构来获取相应的信息,并传递给delete[]操作符。

堆和栈的区别

  • 堆栈空间分配不同:栈由操作系统自行释放,堆一般由程序员释放
  • 缓冲方式不同:堆一般是二级存储,会慢一点,栈是一级存储,函数调用完直接释放
  • 数据结构不同:栈类似栈,而堆类似数组

内存泄漏

  • 父类析构不是虚析构

父类析构函数如果不声明为虚函数,当使用基类指针指向派生类对象并进行delete操作时,只会调用基类的析构函数而不会调用派生类的析构函数,这样就无法正确释放派生类对象中的资源,可能导致内存泄漏或行为未定义。因此,为了确保在继承关系中正确释放资源,通常需要将基类的析构函数声明为虚析构函数。这样当通过基类指针删除派生类对象时,就能够按照继承关系依次调用每个类的析构函数,确保所有资源都得到正确释放。

  • 用malloc或new申请资源后,没有释放
  • share_ptr互相引用对方

说说C++的重载和重写是如何实现的

重载

函数重载的关键是函数的参数列表——也称为函数特征标(function signature)。如果两个函数的参数数目和类型相同,同时参数的排列顺序也相同,则它们的特征标相同,而变量名是无关紧要的。C++允许定义名称相同的函数,条件是它们的特征标不同。如果参数数目和或参数类型不同,则特征标也不同。

重写

在基类的函数前加上virtual关键字,在派生类中重写该函数,运行时将会根据对象的实际类型来调用相应的函数。如果对象类型是派生类,就调用派生类的函数;如果对象类型是基类,就调用基类的函数。

说说 C 语言如何实现 C++ 语言中的重载

使用函数指针来实现,重载的函数不能使用同名称,只是类似的实现了函数重载功能

简述下向上转型和向下转型

子类转换为父类

向上转型(Upcasting)是指将一个派生类的指针或引用转换为它的基类的指针或引用的过程。这种转型是安全的,因为一个派生类对象也是一个基类对象,基类指针或引用可以指向派生类对象。

父类转换为子类

向下转型(Downcasting)是指将一个基类的指针或引用转换为它的派生类的指针或引用的过程。这种转型是不安全的,因为一个基类对象可能不是一个派生类对象,如果对其进行向下转型,可能会导致未定义的行为或内存错误。向下转型应该尽可能避免使用,除非可以确定基类对象是派生类对象。

请问构造函数中的能不能调用虚方法

在 C++ 中,构造函数可以调用虚函数,但是要注意一些细节。在构造函数中调用虚函数时,实际调用的是当前正在构造的对象的虚函数,而不是派生类中重写的虚函数。这是因为在执行派生类的构造函数之前,基类的构造函数会先被执行,此时派生类的对象尚未构造完成,因此调用派生类中的虚函数是不安全的。

那么析构函数中能不能调用虚方法

同上,在析构函数中调用虚函数时,也会按照当前对象的类型来执行,而不是动态绑定到基类的实现

Base constructor
Base virtual method
Derived constructor
可以看到,Base 的构造函数中调用了虚函数,但是实际执行的是 Base 类中的虚函数,而不是 Derived 类中的虚函数。因此,在构造函数中调用虚函数时,需要特别小心,以避免出现问题。

请问拷贝构造函数的参数是什么传递方式,为什么

必须是引用,如果拷贝构造函数中的参数不是一个引用,即形如CClass(const CClass c_class),那么就相当于采用了传值的方式(pass-by-value),而传值的方式会调用该类的拷贝构造函数,从而造成无穷递归地调用拷贝构造函数。因此拷贝构造函数的参数必须是一个引用。

仿函数

把函数当成类用

class SquareFunctor {
public:
    int operator()(int x) {
        return x * x;
    }
};
SquareFunctor square;
int result = square(5); // 计算 5 的平方,结果为 25

C++中类模板和模板类的区别

类模板是模板的定义,不是一个实实在在的类,定义中用到通用类型参数
模板类是实实在在的类定义,是类模板的实例化。类定义中参数被实际类型所代替。

64位系统存一个地址多大空间

64位系统存一个地址的空间大小取决于内存地址的位数。一般来说,一个内存地址对应一个字节(8位),所以64位系统可以表示16个16进制数(64位)的内存地址。这样,64位系统的最大寻址空间为2的64次方字节,即16384PB或16777216TB。但是,并不是所有的64位系统都能使用这么大的寻址空间,因为有些CPU只有40位或48位的地址线,而且操作系统也有自己的限制。

函数传递时会不会在内存拷贝

在C++中,定义函数int function(int a[], int b),这里数组a不会在内存中拷贝,传递的是指针。数组名就是一个指向数组第一个元素的指针,所以当你把数组名作为参数传递时,实际上是传递了一个指针。如果你想要传递整个数组的副本,你可以使用引用或者复制数组的内容到另一个数组。
当你传递一个普通的整型变量(如参数 b)作为函数参数时,实际上会发生值传递。这意味着参数的值会被复制到函数调用的栈帧中,而不是传递参数本身的地址或引用。

为什么要使用友元?

使用友元可以简化代码,提高效率。
举例来说,假设有一个类 A 和一个函数 F,A 中有一个私有成员变量 x,而 F 函数需要访问这个私有变量。如果不使用友元,就只能使用 A 的公有接口来获取 x,这样会增加代码的复杂性和开销。
但是友元函数也有缺点

  • 破坏封装性:

友元函数或类可以直接访问类的私有成员,这样会破坏类的封装性,导致私有成员可以被外部函数或类直接操作,增加了代码的耦合性。

  • 降低可维护性:

友元使得类的实现细节对外暴露,当类的实现发生变化时,所有依赖于友元关系的外部函数或类都可能受到影响,增加了代码的维护成本。

检查内存泄漏的方法

  • 使用第三方工具
  • 重写new和delete,给出关键信息
  • 使用智能指针

C++编译和C编译的区别

链接库:C语言和C++语言使用的链接库不同,C语言使用C标准库,C++语言使用C++标准库。C++标准库中包含了C标准库中的所有函数,同时还包含了STL(标准模板库)和一些面向对象的特性,如命名空间、类、继承等。

如何判断一段函数是C++编译的还是C编译的

#ifdef __cplusplus
// Code being compiled as C++.
#endif

这是一个预处理器指令,它可以让你在编译前对源代码进行一些操作。#ifdef __cplusplus 的意思是如果__cplusplus 宏被定义了,就执行后面的代码。__cplusplus 宏是一个特殊的宏,它只有在C++ 编译器下才会被定义,所以这个指令可以用来检查当前的编译环境是否是C++。#endif 的意思是结束#ifdef 的范围。

如何在不用sizeof的情况下判断系统是多少位

ISWow64Process函数

重复多次 fclose 一个打开过一次的 FILE *fp 指针会有什么结果,并请解释

导致文件描述符结构中指针指向的内存被重复释放,进而导致一些不可预期的异常。

为什么函数传递数组参数,结果数组会被修改,而值不行?

当数组名作为参数时,传递的实际上是地址。
而其他类型如int作为参数时,由于函数参数值实质上是实参的一份拷贝,被调函数内部对形参的改变并不影响实参的值。

main 函数执行以前,还会执行什么代码?

全局对象的构造函数会在main 函数之前执行。

字符指针、浮点数指针、以及函数指针这三种类型的变量哪个占用的内存最大?为什么?

指针变量也占用内存单元,而且所有指针变量占用内存单元的数量都是相同的。32位4个字节,64位8个字节
就是说,不管是指向何种对象的指针变量,它们占用内存的字节数都是一样的,并且要足够把程序中所能用到的最大地址表示出来(通常是一个机器字长)。指针变量的大小固定是因为它们存储的是内存地址,而不是实际的数据。这使得指针变量在不同类型之间具有相同的大小,因为它们只是存储一个内存地址,无论指向的是什么类型的数据,所需的内存空间都是一样的。
当使用指针进行解引用操作时,不同类型的指针会导致不同大小的数据被读取。

C++几个基本类型占用空间

数据类型 16位系统 32位系统 64位系统
bool 1字节 1字节 1字节
char 1字节 1字节 1字节
short 2字节 2字节 2字节
int 2字节 4字节 4字节
float 4字节 4字节 4字节
double 8字节 8字节 8字节
void 不占空间 不占空间 不占空间
wchar_t 2字节 2或4字节5 2或4或8字节

继承时应该要写哪些类的成员函数

C++继承时,一般要写类的构造函数(包括拷贝构造)、析构函数、赋值运算符重载函数,以及其他需要实现类的功能或接口的成员函数

怎样让对象只能创建在栈/堆/内存池中

首先我们要明确一个概念:

默认构造函数创建的对象通常是在调用它的地方所在的上。当你在函数内部声明一个对象并调用默认构造函数时,这个对象会被分配在函数的栈帧中

只允许在栈上创建对象:

如果对象是通过new关键字在堆上创建,那么它将在堆上分配内存;如果对象是通过普通的声明方式在函数中创建,那么它将在栈上分配内存。

法一:构造函数私有化 + 静态的公有接口
静态方法的特点是可以在不实例化类的情况下调用,因此可以直接通过类名来调用该方法。说白了我们就可以不用new来创建对象

//只能在栈上创建的对象
class StackOnly {
public:
	//静态公有方法, 在栈上创建对象
	static StackOnly getobj() {
		StackOnly so;
		return so;
	}

	//拷贝构造不用管, 本来就在栈上

private:
	//构造函数私有化
	StackOnly() {}
};

法二:禁止new
直接把new的方式限制死了,这样就不怕在堆上创建了

class StackOnly {
public:
	//重定义operator new/delete, 定义成删除函数
	void* operator new(size_t n) = delete;
	void operator delete(void* ptr) = delete;
};

只允许在堆上创建对象:

思路和在栈上是一样的,构造函数私有化+公有接口

//只能在堆上创建的对象
class HeapOnly {
public:
	//静态公有的方法,创建对象
	static HeapOnly* getobj() {
		//堆上创建对象
		return new HeapOnly;
	}
private:
	//构造函数私有化
	HeapOnly() { }

	//防拷贝: 
	//1. 声明不实现
	HeapOnly(const HeapOnly& ho);
	//2. 直接定义成delete函数
	HeapOnly(const HeapOnly& ho) = delete;
};


void test() {
	//栈上的对象:自动调用公有的无参默认构造
	//HeapOnly ho;
	
	//堆上的对象
	HeapOnly* ptr = HeapOnly::getobj();
}

只允许在内存池中创建对象:

核心思路就是先把栈和堆的都取消
MyPool类负责内存的分配和释放,MyObject类重载了new和delete运算符,以便将对象的内存管理委托给MyPool类。

class MyPool {
public:
  static void* allocate(std::size_t size) {
    // 自定义内存池分配内存
  }

  static void deallocate(void* ptr, std::size_t size) {
    // 自定义内存池释放内存
  }
};

class MyObject {
public:
  void* operator new(std::size_t size) {
    return MyPool::allocate(size);
  }

  void operator delete(void* ptr) {
    MyPool::deallocate(ptr, sizeof(MyObject));
  }

  void* operator new[](std::size_t size) = delete;
  void operator delete[](void* ptr) = delete;
};

在上面的示例中,MyObject 类重载了 operator new 和 operator delete
运算符,从而实现了只允许在内存池中创建对象的功能。operator new 运算符调用 MyPool::allocate
方法从内存池中分配内存,而 operator delete 运算符调用 MyPool::deallocate 方法释放内存。同时,禁用了
operator new[] 和 operator delete[]
运算符,从而防止在自由存储区创建数组对象。在实际使用时,需要根据具体的内存池实现来修改 MyPool 类中的代码。

RTTI原理,type_info信息存在虚函数表的哪里

RTTI就是运行时动态绑定

同时,编译器也会在每个有虚函数的类对应的type_info对象中存储该类的类型信息。type_info对象是一个标准库提供的类,它包含了类型名称、哈希码等信息,并且重载了==和!=运算符来比较两个类型是否相同。

为了让type_info对象和虚函数表关联起来,编译器会在虚函数表 的开头插入一个指针,指向当前类对应的type_info对象。这样,在运行时就可以通过基类指针p找到vfptr,再通过vfptr找到type_info对象指针,进而取得类型信息。

type_info信息存在虚函数表的开头,也就是虚函数表的第一个元素是一个指向type_info对象的指针 。

C++在哪些情况下会产生临时对象

C++中,临时对象是编译器在不同的情况下创建的没有名字的对象。临时对象通常出现在以下场景:

引用初始化,例如 const int& r = 42;
参数传递,例如 f(42);
表达式求值,例如 a + b;
函数返回,例如 return x + y;
异常抛出,例如 throw x;
临时对象有一个生命周期,由它们的创建点和销毁点决定。任何创建多个临时对象的表达式最终会按照创建的逆序销毁它们。临时对象的销毁时间取决于它们的使用方式:

用于初始化const引用的临时对象:如果引用是局部变量,则在引用离开作用域时销毁;如果引用是类成员,则在类实例被销毁时销毁。
用于初始化非const引用或值类型的临时对象:在表达式结束后立即销毁。

C++静态链接库(lib)和动态链接库(dll)的区别

静态链接库(lib)是在编译时将库的代码直接复制到可执行文件中,所以在程序运行时不需要依赖任何外部库文件,所有的代码都在一个可执行文件中。因此,静态链接库的优点是移植方便,无需安装其他库文件,程序运行时速度较快。缺点是占用硬盘空间较大,同时也存在代码重复的情况,不利于代码的更新和维护。

动态链接库(dll)是在程序运行时才被加载到内存中,程序需要调用库函数时才会加载对应的库文件。因此,动态链接库的优点是共享库文件,节省了硬盘空间,同时也方便了库文件的更新和维护。缺点是相对于静态链接库,程序运行时会存在一定的额外开销,如加载库文件、解析符号等。

memory_move和memory_copy是什么,他们的区别?

“memory_move” 和 “memory_copy” 是用于处理内存中数据的操作,用于移动或复制内存中的数据。

memcpy 的实现比较简单,它只是简单地把数据从源地址按字节逐一复制到目标地址。这意味着,如果源地址和目标地址存在重叠,即它们指向同一块内存区域,那么 memcpy 可能会出现未定义的行为,也就是数据会被错误地覆盖。因此,使用 memcpy 时需要确保源地址和目标地址不会发生重叠。

而 memmove 的实现则更为复杂,它能够处理源地址和目标地址重叠的情况,即使这两个地址相互重叠,也能够保证正确地复制数据。具体来说,memmove 在复制数据时会先把数据拷贝到一个临时缓冲区中,然后再把数据从缓冲区复制到目标地址,这样就避免了源地址和目标地址重叠时数据被错误地覆盖的问题。但是,由于要使用临时缓冲区,所以 memmove 的性能可能会稍微低一些。

  • 121
    点赞
  • 1056
    收藏
    觉得还不错? 一键收藏
  • 17
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值