C++知识点总结(三)

 之前写的总结:

C++知识点总结(一)

C++知识点总结(二)

C++知识点总结(三)


 目录 

目录

 目录 

二十一、数组和指针

选择建议: 

二十二、类型转换

1. static_cast

适用于

 注意事项:

2.dynamic_cast

适用于

条件

注意事项

3.const_cast

适用于

注意事项

4.reinterpret_cast

适用于

注意事项

四种类型转换总结表格

二十三、虚函数

1.工作机制

2.为什么析构函数要声明为虚函数

3.为什么构造函数不能是虚函数

4.还有哪些函数不能是虚函数

二十四、多态、纯虚函数和抽象类

多态

纯虚函数

抽象类

二十五、虚继承

1.实现原理

2.解决的问题 

3.特性

4.注意事项

二十六、函数重载和重写

1.函数重载

定义

特性

优点

2.函数重写

定义

特性

二十七、什么时候用指针,什么时候用引用

1.指针

常见场景

2.引用

常见场景

二十八、迭代器:++it和it++哪个好

1. 前置自增操作符(++it)

行为

优点

2. 后置自增操作符(it++)

行为

缺点

二十九、构造函数的执行顺序

三十、字符串从开始打印到屏幕上的全过程

总结


二十一、数组和指针

数组:

  • 静态分配:在栈上分配连续内存,无法根据需求动态调整大小。
  • 固定大小:数组的大小在编译时确定。
  • 编译器优化:由于分配在栈上,访问速度快,编译器可以更好地优化。
  • 局限性:如果大小过大或无法预测,可能导致栈溢出。
  • 强类型检查:数组绑定特定类型,访问时编译器会检查类型是否匹配。
  • 隐式转换:不支持隐式类型转换,使用更安全。 

指针:

  • 指针本身:占用固定大小的内存(根据系统的地址长度,为4字节或8字节)。
  • 动态分配:通过new或malloc分配堆内存,内存大小可以在运行时指定。
  • 灵活性:可以指向任何内存位置(栈、堆、静态内存等),也可以强制转换为其他类型,容易引发类型安全问题。
  • 内存管理:需要手动释放分配的堆内存(避免内存泄漏)。
特性数组指针
内存分配静态分配,速度快动态分配,灵活但需管理
大小灵活性固定大小,声明时确定动态分配,运行时可调整
类型安全性强类型检查,安全性高 类型转换灵活,可能不安全
操作灵活性无法直接算术运算,偏严格支持指针运算,访问灵活
内存管理自动释放,无需手动管理需手动释放内存,否则可能泄露

选择建议: 

如果大小固定且易于管理,优先使用数组。

如果需要动态大小或跨函数共享,使用指针。

二十二、类型转换

1. static_cast

 用于在有明确定义的类型之间进行转换。

适用于

  • 基本数据类型的转换(如int转换成float)
  • 类层次结构中,从派生类到基类的安全转换
  • 忽略const和volatile的限定符。
  • 转换非多态类型的指针。
#include<iostream>
usingn namespace std;

class Base {};
class Derived : public Base {};

int main(){
    // 基本类型转换
    int a = 10;
    float b = static_cast<float>(a);
    cout << "b: " << b << endl;

    // 类指针转换
    Derived d;
    Base* basePtr = static_cast<Base*>(&d); // 派生类 -> 基类
    cout << "Pointer conversion successful!" << endl;

    return 0;
}

 注意事项:

  • 不能用于从基类指针转换成派生类指针(非安全转换)。
  • 不会进行运行时类型检查。

2.dynamic_cast

专用于多态类型的指针或引用转换

适用于

  • 从基类指针转换为派生类指针。
  • 检查运行时类型安全性。

条件

  • 必须在类中包含至少一个虚函数
  • 转换失败时,指针返回nullptr,引用抛出bad_cast异常。
#include <iostream>
using namespace std;

class Base {
public:
    virtual void func() {} // 必须有虚函数
};

class Derived : public Base {};

int main() {
    Base* basePtr = new Derived();
    Derived* derivedPtr = dynamic_cast<Derived*>(basePtr);

    if (derivedPtr != nullptr) {
        cout << "Conversion successful!" << endl;
    } else {
        cout << "Conversion failed!" << endl;
    }

    delete basePtr;
    return 0;
}

注意事项

  • 转换代价较高,避免频繁使用
  • 如果类型层次不明确或对象不是派生类的实例,转换会失败

3.const_cast

用于移除或添加const、volatile属性

适用于

  • 常用于需要对const对象进行非常规修改的场景。
#include <iostream>
using namespace std;

void modify(const int* ptr) {
    int* modifiablePtr = const_cast<int*>(ptr); // 移除 const
    *modifiablePtr = 20; // 修改值
}

int main() {
    const int a = 10;
    cout << "Before: " << a << endl;
    modify(&a);
    cout << "After: " << a << endl; // 输出可能是 20,但未定义行为
    return 0;
}

注意事项

  • 修改const对象的值可能导致未定义行为,特别是当该对象被存储在只读内存中。

4.reinterpret_cast

用于在无关类型之间的强制转换

适用于

  •  指针类型之间的转换
  • 将整数转换为指针,或指针转换为整数
  • 不提供类型安全性检查,仅改变数据的解释方式
#include <iostream>
using namespace std;

int main() {
    long p = 0x12345678;
    int* ptr = reinterpret_cast<int*>(p); // 强制将 long 转为 int*
    cout << "Pointer: " << ptr << endl;

    // 任意类型转换
    int x = 10;
    float* fptr = reinterpret_cast<float*>(&x);
    cout << "Reinterpreted float: " << *fptr << endl; // 未定义行为

    return 0;
}

注意事项

  • 这种转换不检查安全性,可能导致未定义行为
  • 应尽量避免使用,除非明确知道转换的意义。

四种类型转换总结表格

转换类型功能使用场景运行时检查安全性
static_cast用于相关类型之间的显式转换基本类型转换、派生类到基类指针的转换较高
dynamic_cast多态类型间的安全转换基类指针到派生类指针的转换
const_cast添加或移除const和volatile需要修改const对象(非常规操作)较低,需谨慎使用
reinterpret_cast在几乎无关类型之间进行的低级强制转换指针类型转换、指针与整数间的转换最低、慎用!

二十三、虚函数

允许在基类中通过virtual声明一个函数,然后在派生类中对它进行重定义。

实现虚函数关键在于虚函数表(vtable)虚函数表指针(vptr)

1.工作机制

 每个含有虚函数的类都有一张虚函数表,表中存有该类的虚函数的地址。每个对象都有一个虚函数表指针来指向这个类的虚函数表。当调用虚函数时,程序会通过对象的虚函数表指针找到相应的虚函数地址,然后进行函数调用。

注:虚函数的调用比普通函数多了一个vtable查找过程,运行时有额外开销。

2.为什么析构函数要声明为虚函数

 如果基类的析构函数不是虚函数,在销毁派生类对象时,只有基类的析构函数会被调用,而派生类的析构函数不会被调用,可能导致资源泄露。

#include <iostream>
using namespace std;

class Base {
public:
    ~Base() { cout << "Base destructor" << endl; }
};

class Derived : public Base {
public:
    ~Derived() { cout << "Derived destructor" << endl; }
};

int main() {
    Base* ptr = new Derived();
    delete ptr; // 错误调用,仅调用 Base 的析构函数
    return 0;
}

解决方法

将基类析构函数声明为虚函数

class Base {
public:
    virtual ~Base() { cout << "Base destructor" << endl; }
};

3.为什么构造函数不能是虚函数

  • 构造函数用于初始化对象,而在对象构造阶段时虚函数表尚未完全建立
  • 如果构造函数是虚函数,派生类无法正确调用其虚函数,导致逻辑混乱

4.还有哪些函数不能是虚函数

  • 内联函数------内联函数表示在编译阶段进行函数体的替换,而虚函数在运行期间进行类型确定
  • 静态函数------静态函数不属于类的某个对象,没有this指针,无法访问虚函数表
  • 友元函数------友元函数是类外定义的,不属于类成员,不能出现在虚函数表中
  • 普通函数------普通函数与类没有关系,不能加入类的虚函数表中

二十四、多态、纯虚函数和抽象类

多态

通常依赖于虚函数,在基类中声明虚函数,然后在派生类中进行重写。

 在继承中构成多态的条件:

  • 必须通过基类的指针或者引用调用虚函数
  • 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写

纯虚函数

在虚函数后面写上=0,相当于一个接口名,不提供任何实现。

作用 

  • 用于定义接口,派生类必须重写纯虚函数
  • 让基类充当一个抽象类,以确保派生类实现特定的功能

抽象类

含有纯虚函数的类称为抽象类

特性

  • 不能实例化
  • 可以包含普通成员函数
  • 可以定义构造函数和析构函数
#include <iostream>
using namespace std;

class AbstractBase {
public:
    virtual void display() = 0; // 纯虚函数
    virtual ~AbstractBase() { cout << "AbstractBase destructor" << endl; }
};

class Derived : public AbstractBase {
public:
    void display() override { cout << "Derived display" << endl; } // 实现纯虚函数
};

int main() {
    // AbstractBase obj; // 错误:抽象类不能实例化
    AbstractBase* ptr = new Derived(); // 可以通过基类指针操作派生类对象
    ptr->display();                    // 调用 Derived 的 display
    delete ptr;
    return 0;
}

二十五、虚继承

1.实现原理

  • 虚继承机制------编译器在使用虚继承时,会在派生类中添加一个指针,指向唯一的基类子对象。(但会增加运行时的间接访问开销,但消除了重复继承问题)
  • 对象模型------(1)在普通继承中,每个派生类会独立包含基类的成员(2)在虚继承中,基类的成员存储在共享的基类子对象中,所有虚继承的派生类都指向这个子对象。

2.解决的问题 

 菱形继承问题

 假设有四个类:A、B、C和D,其中B和C都继承自A,而D又同时继承自B和C。这样就会形成一个“菱形”结构,D类中会包含两个独立的A类子对象,导致冗余

3.特性

  • 避免重复继承问题------基类子对象在最终的派生类中只存在一份
  • 解决二义性------访问基类成员时不需要指定路径
  • 灵活性------虚继承适合用于复杂的继承体系,确保基类子对象不重复

4.注意事项

  • 构造顺序问题------在虚继承中,基类的构造函数会有最终的派生类负责调用,中间派生类的构造函数不能直接初始化虚基类
  • 运行时开销------虚继承增加了对象的内存布局复杂性,以及成员访问的运行时开销
class A {
public:
    A(int v) { cout << "A constructed with value " << v << endl; }
};

class B : virtual public A {
public:
    B() : A(0) {} // 错误,B 无法直接初始化虚基类 A
};

class D : public B {
public:
    D() : A(10), B() {} // 正确,D 初始化虚基类 A
};

二十六、函数重载和重写

1.函数重载

定义

函数重载是指在同一个作用域内,可以定义多个函数名相同但参数不同(个数或类型不同)的函数。注:函数重载根据参数类型和个数进行区分,返回类型不同并不算重载依据。

特性

  • 同名函数:重载函数的函数名相同
  • 参数不同:重载函数的参数个数或参数类型不同
  • 返回类型:仅通过返回类型无法区分重载函数,返回类型不构成重载。

优点

  • 增加代码可读性
  • 改善程序可维护性
#include <iostream>
using namespace std;

class Printer {
public:
    void print(int x) {
        cout << "Printing integer: " << x << endl;
    }
    
    void print(double x) {
        cout << "Printing double: " << x << endl;
    }
    
    void print(const string& str) {
        cout << "Printing string: " << str << endl;
    }
};

int main() {
    Printer p;
    p.print(10);         // 调用 print(int)
    p.print(3.14);       // 调用 print(double)
    p.print("Hello");    // 调用 print(string)
    return 0;
}

2.函数重写

定义

函数重写发生在 继承关系 中,子类通过重写父类的方法来实现自己的具体实现。重写函数需要与父类的函数签名(包括参数列表和返回类型)完全一致。

特性

  • 发生在继承中:重写发生在子类中,子类函数与父类函数具有相同的名称和签名。
  • 完全一致的签名:子类重写的函数必须与父类的函数具有相同的函数名、参数列表和返回类型。
  • 动态绑定:当父类的指针或引用指向子类对象时,调用的是子类重写后的方法,这种行为在运行时决定。
#include <iostream>
using namespace std;

class Animal {
public:
    virtual void sound() {
        cout << "Animal makes a sound" << endl;
    }
};

class Dog : public Animal {
public:
    void sound() override { // 重写父类的 sound 方法
        cout << "Dog barks" << endl;
    }
};

int main() {
    Animal* animal = new Dog();
    animal->sound();   // 运行时调用 Dog 类的 sound(),动态绑定
    delete animal;
    return 0;
}

二十七、什么时候用指针,什么时候用引用

1.指针

是一个变量,存储另一个变量的内存地址。

常见场景

  • 动态内存管理------需要在运行时动态分配内存时,需要使用指针。例如,在使用 newmalloc 分配堆内存时。
  • 可以指向nullptr------指针可以为空,表示没有指向任何对象
  • 对象的传递或返回------当函数需要传递对象时,并且你可能希望修改传入对象的内容或避免复制时,使用指针。可以返回多个值或指向多个对象。
  • 数组和字符串的处理-------数组和字符串本质上是指向元素的指针,指针可以用来遍历数组或字符串。
  • 指向基类或派生类-------当你需要通过指针访问不同类型的对象(例如基类指针指向派生类对象)时,可以使用指针。

2.引用

 是对另一个对象的别名,使用引用时无需解引用操作,语法更简洁。必须在初始化时绑定到一个对象,绑定过后不能再更改。

常见场景

  • 函数参数传递------希望避免对象的复制并且允许修改传入的对象时,使用引用。引用提供了比指针更简洁的语法,并且不会为空。
  • 常量引用传递------希望避免复制,但又不希望修改对象时,可以使用常量引用
  • 避免空值------引用不能为空
  • 返回值-------如果一个函数需要返回一个对象的引用(通常是类的成员变量或局部静态对象),则使用引用返回值,而非指针
  • 链式调用
    std::ostream& operator<<(std::ostream& os, const MyClass& obj) {
        os << obj.toString();
        return os;
    }
    

二十八、迭代器:++it和it++哪个好

1. 前置自增操作符(++it)

首先将对象自增,然后返回对当前对象的引用。

int& operator++(){
    *this+=1;      //首先进行自增操作
    return *this;    //返回当前对象的引用
}

行为

自增操作会直接作用于对象本身,然后返回对象的引用(*this)。

优点

  • 前置自增操作不会产生临时对象,因此效率较高
  • 直接返回引用,可以用于链式调用,例如++(++it)

2. 后置自增操作符(it++)

首先创建一个当前对象的临时副本,然后执行自增操作,最后返回副本

int operator++(){
    int temp=*this;    //保存当前对象的副本
    ++*this;      //调用前置自增
    return temp;    //返回副本
}

行为

自增操作会直接将当前对象的值存储在一个临时变量中,然后调用前置自增原对象,最后返回那个临时对象的副本。后置自增返回对象的副本,而不是引用,因此会涉及到拷贝。

缺点

  • 由于返回的是副本,后置自增会产生额外的临时对象,导致性能下降,特别是在自增操作发生在大量循环或复杂对象上时。

二十九、构造函数的执行顺序

1.虚基类构造函数(如果有虚继承)

2.上层基类构造函数(按从上到下的顺序)

3.vptr初始化

4.成员变量初始化(按照类中声明的顺序,而不是初始化列表的顺序)

5.构造函数体内的代码

三十、字符串从开始打印到屏幕上的全过程

1.用户告诉操作系统执行HelloWorld程序

2.操作系统 : 找到HelloWorld程序的相关信息,检查其类型是否为可执行文件,并通过程序首部信息,确定代码和数据在可执行文件中的位置并计算出对应的磁盘块位置

3.操作系统 : 创建一个新进程,将可执行文件映射到该进程结构。

4.操作系统 : 为该程序设置CPU上下文环境,并跳到程序开始处。

5.执行HelloWorld程序的第一条指令,发生缺页异常。

6.操作系统 : 分配一页物理地址,并将代码从磁盘读入内存,然后继续执行程序。

7.程序执行puts函数,在显示器上写入。

8.操作系统找到要将字符串送往的显示设备,发送给控制该设备的进程。

9.操作系统 : 控制设备的进程告诉设备的窗口系统所要显示的字符串。窗口系统确认操作后将字符串转换成像素,并写入设备的存储映像区

10.视频硬件将像素转换成显示器可接收和一组控制数据信号

11.显示器解释信号,激发液晶屏

12.显示成功。


总结

 究极折磨,后天联通笔试,再往后还有中石化笔试。什么时候才是个头啊!

加油,争取还是一天一更!!!!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值