C++进阶

一、细节

1.cout与输出缓冲区

输出缓冲区是内存中的一块区域,用于临时存储将要输出的数据。这种缓冲机制有助于提高性能,因为直接进行I/O操作(如写入控制台)通常比操作内存慢得多。

缓冲区的刷新(flush)是将缓冲区中的内容实际写到输出设备(如控制台)上的过程。在以下情况下,缓冲区会被刷新:

  1. 缓冲区满了。
  2. 程序正常结束时。
  3. 调用特定的刷新函数(如 flush)。
  4. 输出流遇到某些特殊字符(如 \nendl)。

\nendl

  • \n 是换行符,用于在输出中插入一个换行。它不会立即刷新缓冲区,只是将换行符添加到缓冲区中。
  • endl 是一个操控符,它不仅会在输出中插入一个换行,还会强制刷新缓冲区。

2.constexpr

constexpr 是C++11引入的关键字,用于在编译时计算常量表达式(constant expressions)(避免运行时进行计算,提高程序性能)。它可以用于变量、函数和构造函数,帮助提高程序的性能和安全性。

image-20240626140224668

image-20240626140403419

constexpr 函数是可以在编译时求值的函数,这要求函数的所有操作都必须是常量表达式,并且函数体内不能包含任何可能导致运行时执行的操作。constexpr 函数可以用于编译时和运行时计算。

image-20240626141045512

image-20240626141117732

constexpr 构造函数允许类对象在编译时创建。这要求类的所有成员变量都必须是常量表达式,且构造函数本身不能进行任何运行时操作。

image-20240626141339602

image-20240626141408372

3.NULL和nullptr是不同的类型

虽然用NULL和nullptr赋值的指针可以相互比较,但是他们是不同的类型,在C++中用nullptr来给指针赋初值,因为NULL实际是整型数0。

image-20240626150614086

image-20240626150939935

4.关于inline

首先看一个例子,假设有一个这样的头文件,有防卫式声明,被不同的源文件包含。

image-20240626163355155

image-20240626163404578

image-20240626163412875

编译显示func重定义。
image-20240626163437558

project1.cppproject2.cpp 分别包含 func.h 时,每个编译单元(translation unit)都会包含 func 函数的定义。因此,在链接阶段(linking phase),链接器会发现有多个相同的 func 定义,从而导致重定义错误。因此,我们都是将函数的声明放在头文件中,定义放在源文件中。

如果在函数前加inline呢?
image-20240626163556661

  • inline 关键字提示编译器将函数的定义插入到调用点,而不是生成一个单独的函数调用。尽管如此,编译器并不总是会内联展开函数,它只是将其作为一个建议。
  • 更重要的是,inline 允许在多个编译单元中定义相同的函数,而不会引起重定义错误。这是因为 C++ 允许内联函数在多个编译单元中有相同的定义,并会在链接时进行特殊处理以避免重定义错误。
  • **内联函数的定义必须放在头文件中。**这是因为内联函数需要在每个使用它的编译单元(translation unit)中可见,以便编译器能够在调用点进行展开或者至少知道该函数的定义。

5.函数杂合用法

  • 函数返回类型是void,表示函数不返回任何类型。但是我们可以让一个返回类型是void的函数作为另一个返回类型是void的函数的返回值。

image-20240626164500325

  • 函数返回引用的情况。

image-20240626174626286

返回局部变量的引用,往一个不属于你的地址写了数据,虽然编译成功并且运行了,但是如果程序有地方用到这块内存空间,就会造成程序崩溃。

但是编译器对引用做了特殊处理,如果返回类型是引用,但是我们用普通类型去接受函数返回值,那么函数返回的就是值而不是引用。此时就相当于用10这个值赋给了变量a,然后a又将自身的值修改为20。
image-20240626174921518

  • 如果一个函数我们不调用的话,可以只有声明,不需要实现。

6.const char*、char const*、char * const

image-20240626180943953

这也就引申出我们为什么会在某些函数形参中加入const修饰。

image-20240626181615525

顶层 const 和底层 const

  • 顶层 const(Top-level const):
    • 修饰指针本身,使指针本身为常量,即指针的地址不能改变。
    • 示例:int* const p 中的 const 是顶层 const,表示 p 是一个常量指针。
  • 底层 const(Low-level const):
    • 修饰指针指向的对象,使指针指向的对象为常量,即不能通过指针修改指向的数据。
    • 示例:const int* p 中的 const 是底层 const,表示 *p 是常量。

7.进程地址空间,所谓静态区常量区不准

OIP-C

  1. Text Segment(文本段或代码段):这部分内存存放的是程序的机器代码,即编译后的程序。这部分是只读的,主要是为了防止程序代码被意外修改。
  2. Data Segment(数据段):存放初始化的全局变量和静态变量。这部分内存在程序开始执行时由编译器初始化,并在程序运行期间持续存在。
  3. BSS Segment(未初始化数据段):用于存放程序中未初始化的全局变量和静态变量。在程序启动时,操作系统会将此段内存初始化为零。
  4. Memory Mapping Segment(内存映射段):这部分内存通常用于映射外设的内存空间或实现文件映射。共享库。
  5. Stack(栈):用于存放函数的局部变量、函数参数和返回地址等。每当调用一个函数时,其相关信息就会被推送到栈上,函数返回时信息又会被弹出。栈具有后进先出的特性。
  6. Kernel Space(内核空间):这部分内存是操作系统保留的,用于运行内核代码和处理器内核模式下的操作。用户程序通常不能直接访问内核空间。

image-20240629084130454

8.位运算

#include<bitset>

输出二进制内容:

image-20240629091423980

image-20240629111430166

image-20240629111452600

异或运算的特点:

异或运算(XOR),表示为 ^,有几个关键特性:

  1. 一个数与自身异或的结果是0,即 x ^ x = 0
  2. 一个数与0异或的结果是数本身,即 x ^ 0 = x
  3. 异或运算满足交换律和结合律,即 a ^ b = b ^ aa ^ (b ^ c) = (a ^ b) ^ c

image-20240629094132536

image-20240629094554171

第一步: a = a ^ b; 这一步中,我们将 ab 进行异或运算,并将结果存储在 a 中。现在 a 存储的是原始 ab 异或的结果,我们可以称之为 a'。此时,aab 的信息混合体。

第二步: b = a ^ b; 在这一步中,新的 a (即 a') 与原始的 b 进行异或运算。根据异或的性质,由于 a' 包含了原始 ab 的信息,再与 b 异或相当于 b 与自己的信息抵消,留下了原始的 a。因此,这一步的结果是将原始的 a 值赋给了 b

第三步: a = a ^ b; 最后一步,现在的 a (即 a') 与现在的 b(即原始的 a)进行异或运算。由于 a' 是原始的 ab 的异或结果,与原始的 a 异或会抵消 a 的部分,留下原始的 b。因此,这一步的结果是将原始的 b 值赋给了 a

9.多态

9.1 内存切片

在面向对象编程中,多态允许我们通过基类指针或引用来调用派生类的方法。内存切片(Object Slicing)是多态中的一个问题,它发生在一个派生类对象被赋值给一个基类对象时。

内存切片现象解释:

当派生类对象通过值(而非指针或引用)赋值给基类对象时,派生类对象的额外属性(即派生类特有的成员)会被“切掉”,只剩下基类部分的成员数据被复制到基类对象中。这种现象就称为“内存切片”。

为什么会发生内存切片:

这是因为当通过值赋值时,赋值操作符只能处理目标对象(基类对象)类型的数据大小由于派生类可能包含额外的成员变量或方法,这些无法通过基类对象来存储,因此在赋值过程中这部分额外的信息就会丢失。

内存切片可能导致以下问题:

  1. 数据丢失:派生类特有的数据成员在赋值过程中丢失,这可能导致程序逻辑错误。
  2. 多态失效:由于派生类的特定行为(通过重写的方法实现)被切掉,使用基类对象调用方法时,不能表现出预期的多态行为。

假设有一个基类 Base 和一个从 Base 派生的类 DerivedDerived 类添加了一些新的成员变量和方法。

class Base {
public:
    int base_var;
    virtual void print() { cout << "Base class" << endl; }
};

class Derived : public Base {
public:
    int derived_var;
    void print() override { cout << "Derived class" << endl; }
};

如果我们创建一个 Derived 类的对象,并试图将其赋值给一个 Base 类的对象,就会发生内存切片:

Derived d;
d.derived_var = 10;
Base b = d;  // 这里发生内存切片,derived_var 信息丢失
b.print();  // 输出 "Base class"

在这个例子中,derived_var 的值不会存在于 b 中,因为 b 只是一个 Base 类型的对象,不包含 Derived 类的任何额外信息。

如何避免内存切片?

  • 使用指针或引用来维护多态行为,这样可以保证对象的完整性不被破坏。
  • 避免将派生类对象赋值给基类对象。

9.2 转型

向上转型 (Upcasting):

向上转型是将派生类的指针或引用转换为基类的指针或引用。这种转换是安全的,因为每个派生类对象都是一个基类对象,所以基类的部分是派生类对象的一部分。这种转换通常在多态中自动进行。

向下转型 (Downcasting):

向下转型是将基类的指针或引用转换为派生类的指针或引用。这种转换是不安全的,因为不是所有基类对象都是派生类对象。向下转型常常需要程序员确保这种转换是合理的,并且通常需要使用强制类型转换,如 dynamic_cast

9.3 构造函数和析构函数里是静态绑定

image-20240629150524290

构造函数总是静态绑定的。这意味着构造函数的调用不是基于对象的运行时类型,而是基于对象声明时的类型。构造函数的静态绑定确保了在创建派生类对象时,可以从基类到派生类正确地、按顺序初始化对象的各部分。

析构函数默认是静态绑定的,但如果将析构函数声明为虚函数,则会变成动态绑定。声明析构函数为虚函数是管理继承关系中的对象时的一个重要实践,因为它确保了当通过基类指针删除派生类对象时,对象的资源可以被适当地释放。

image-20240629151227909

autofunMainObject 类中的一个非虚成员函数,它内部调用了 func() 方法。

尽管 autofun() 定义在基类 MainObject 中,但是在它内部调用的 func() 方法是虚拟的(virtual)。这意味着调用的 func() 方法将会是对象 ob 运行时类型的版本,即 OtherObjectfunc()

由于 func() 方法在 MainObject 类中被声明为虚函数,因此 C++ 的动态绑定机制确保了在通过 ob.autofun() 调用时,实际上运行的是 OtherObjectfunc() 方法。即使调用发生在基类 MainObject 的上下文中,多态性质保证了派生类方法的调用。

9.4 dynamic_cast的跨类转换与向下转换,以及利用typeid调试多态

假设继承关系如下,我们想要从类Main转换到类Y

image-20240629161037713

image-20240629161404261

此时,跨类转换是成功的,因为y不是空指针。

image-20240629161525469

如果项目有很多的继承关系嵌套,那么我们跟踪调试的时候很难确定此时的指针究竟是哪一个类,因此可以用typeid打印类的信息,注意这时候指针需要解引用,否则等号右边需要比较Y*。

9.5 类的成员函数的函数指针语法

成员函数不同于普通函数,它需要一个对象上下文来访问成员变量和其他成员函数,这就是为什么需要使用特殊的语法来处理类成员函数指针(this指针)。

使用对象指针和成员函数指针调用。

image-20240629193125816

使用对象实例和成员函数指针调用。

image-20240629193147559

9.6 类中的虚函数表以及内存破坏

在C++中,虚函数是通过虚函数表(vtable)实现的,这是一个存储指向虚函数地址的指针数组。每个含有虚函数的类都有一个虚函数表。当类的对象被创建时,对象内部会包含一个指向其虚函数表的指针(通常称为vptr)。通过这种机制,当调用对象的虚函数时,实际上是通过虚函数表来动态确定要调用的函数。

我们可以看到,对于有虚函数的类,类的对象的大小多出了4个字节。

image-20240629200756174

这里我们用多态的方式调用一下虚函数,查看一下反汇编。

image-20240629201208486

image-20240629201358479

可以看见,对于第一个函数,这里用b指向的地址来进行函数调用。

image-20240629201625737

对于第二个函数,在第一个函数的地址上偏移了四位。

我们打印一下虚函数表的地址,以及表中前两个函数的地址。

image-20240629203023080

通过汇编查看一下地址是否正确。

image-20240629202547246

image-20240629202625511

image-20240629195221198

虚函数表的性质:

同一个类的多个实例都指向同一个虚函数表。

通过修改虚函数表的数据可以实现劫持。

只有通过指针访问函数才会调用虚函数表(多态的动态绑定)。

image-20240629203701709

image-20240629203744705

定义类的非指针对象,不涉及多态,没有虚函数表,因此调用是正常的。

…(img-3KUljj2O-1719664931428)]

[外链图片转存中…(img-XKAKTYTQ-1719664931428)]

虚函数表的性质:

同一个类的多个实例都指向同一个虚函数表。

通过修改虚函数表的数据可以实现劫持。

只有通过指针访问函数才会调用虚函数表(多态的动态绑定)。

[外链图片转存中…(img-q3WjviHA-1719664931428)]

[外链图片转存中…(img-sL7z2gm5-1719664931428)]

定义类的非指针对象,不涉及多态,没有虚函数表,因此调用是正常的。

image-20240629203920898

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值