没有理想的人不伤心

C++ 易错知识点整理

2024/11/14
3
0

段错误

指针需要先分配内存,否则会指向不确定的位置,可能会引发段错误

因此不要直接定义指针,而是应该定义变量来分配内存。

函数重载、函数重写和运算符重载

1731846530502-f8ea236b-3714-4ca0-82a3-d4b51608265b.png

运算符重载和函数重载类似,不同点在于运算符重载使用 operator 关键字

如:

bool operator==(const Person& other)const {
    return name == other.name && age == other.age;
}

通常在类中进行运算符重载。

函数指针、回调函数、指针函数

函数指针

函数指针是指向函数的指针,允许我们通过指针调用函数。函数指针的类型必须与它指向的函数的签名一致,包括返回类型和参数类型。

// 声明一个指向返回 int 类型、接受两个 int 参数的函数的指针
int(*funcPtr)(int,int);

// 定义一个函数
int add(int a,int b) {
    return a + b;
}

int main() {
    funcPtr = add; // 将函数地址赋给函数指针
    int result = funcPtr(3,4); // 通过指针调用函数
    std::cout << "Result: " << result << std::endl; // 输出:7
}

回调函数

回调函数是一种特殊用途的函数指针,通常被作为参数传递给另一个函数以供调用。回调函数通常用于异步处理或在不确定的情况下调用某个特定的处理逻辑(如事件处理)。

// 定义回调函数类型
typedef void(*Callback)(int);

// 实现一个回调函数
void myCallback(int val) {
    std::cout << "Callback called with value: " << val << std::endl;
}

// 接受回调函数的函数
void process(Callback callback,int value) {
    // 调用回调函数
    callback(value);
}

int main() {
    // 将回调函数传递给另一个函数
    process(myCallback,10); // 输出:Callback called with value:10
}

指针函数

指针函数是一个返回指针的函数,和前面提到的函数指针不同。指针函数用于返回一个指针,但自身并不是函数指针。它在函数签名中带有指针符号(*)。

// 返回 int 类型指针的函数
int* getPointer(int& a) {
    return &a; // 返回变量 a 的地址
}

int main() {
    int x = 10;
    int* ptr = getPointer(x); // ptr 指向 x
    std::cout << *ptr << std::endl; // 输出:10
}

总结

回调函数依赖于函数指针的机制,而指针函数与这两者无直接关系。

  • 函数指针 是一种可以指向函数地址的指针,允许我们通过指针调用函数。
  • 回调函数 是函数指针的一个应用场景,用于将一个函数传递给另一个函数,以便在某个时刻调用它。
  • 指针函数 是返回指针的函数,和函数指针无关,仅是返回值为指针的普通函数。

static 关键字

  1. 静态变量(函数内)
  • 定义在函数内部的 static 变量只会在第一次调用函数时初始化,并在程序的生命周期内保持其值,即使函数执行结束它的值也不会丢失。
  • 这类变量的生命周期贯穿整个程序,但其作用域仍然仅限于所在函数内。
  1. 类的静态成员变量
  • static 成员变量属于整个类而不是类的某个对象,因此所有对象共享同一个 static 成员变量。
  • 静态成员变量必须在类定义体外初始化,因为它们不属于某个对象,而是属于整个类。
  1. 静态成员函数
  • 静态成员函数只能访问静态成员变量,不能访问非静态成员变量(因为非静态成员变量依赖于特定对象)。
  • 它们可以直接通过类名调用,而不需要创建对象。
  1. 静态全局变量(文件作用域)
  • 在文件的全局作用域定义的静态变量,其作用域仅限于定义它的文件,其他文件无法访问它。
  • 它们的生命周期贯穿整个程序,因此可以在文件内部的多个函数之间共享。
  1. 静态全局函数(文件作用域)
  • 类似静态全局变量,在全局作用域定义的 static 函数其作用域也仅限于定义它的文件,其他文件无法访问它。
  • 这种用法在大型项目中常用于模块化代码,限制函数的可见性以避免命名冲突。

const 关键字

用于定义常量, 或限制变量、指针、函数的修改权限,以提高代码的安全性和可读性

常量变量

将变量声明为 const 表示它的值一旦初始化后就不能再更改。

指针常量和常量指针

在使用指针时,const 关键字可以指定指针指向的内容不可更改,或者指针本身不可更改。

指向常量的指针(pointer to const)——如: const int* p = &x;

  • 指针指向的内容不可更改,但指针本身可以指向其他对象。
const int* p = &x; // p 指向的内容不可更改,但 p 可指向其他地址
*p = 20;           // 错误,不能修改指向内容
int y = 30;
p = &y;            // 可以更改 p 的指向

常量指针(const pointer)——如: int* const p = &x;

  • 指针的地址不可更改,但可以修改指向的内容。
int x = 10;
int* const p = &x; // p 是常量指针,不可指向其他地址
*p = 20;           // 可以修改 p 指向的内容
p = &y;            // 错误,不能更改 p 的指向

指向常量的常量指针(const pointer to const)——如: const int* const p = &x;

  • 指针的地址和指向的内容都不可更改。
const int* const p = &x; // p 和 p 指向的内容都不可更改
*p = 20;                 // 错误,不能修改指向内容
p = &y;                  // 错误,不能更改 p 的指向

常量成员函数

  • 在类中定义 const 成员函数,表示该函数不能修改对象的任何成员变量(除了 mutable 修饰的变量)。
  • const 成员函数只能调用其他 const 成员函数。

const 不用于修饰普通函数,对于普通函数往往用来修饰其返回值和传入参数

class MyClass {
public:
    int getValue()const { return value; } // const 成员函数
    void setValue(int val) { value = val; } // 非 const 成员函数

private:
    int value = 0;
};

int main() {
    MyClass obj;
    obj.getValue(); // 可以调用 const 成员函数
    obj.setValue(10); // 可以调用非 const 成员函数
}

常量对象

const 对象只能调用其 const 成员函数,因为 const 对象不允许被修改。

const MyClass obj;
obj.getValue();  // 可以调用 const 成员函数
obj.setValue(10); // 错误,不能调用非 const 成员函数

函数参数中的 const

将函数参数声明为 const 可以防止在函数内部修改传入的值,常用于传入大对象时避免副本带来的开销。

常量引用参数

  • 通过 const 引用传递,可以防止函数内部修改参数,且避免了值传递带来的性能损耗。
void printValue(const MyClass& obj) {
    std::cout << obj.getValue() << std::endl;
}

常量指针参数

  • const 指针参数可以防止函数内部修改指针指向的内容。
void processValue(const int* p) {
    // *p = 10; // 错误,不能修改指向内容
}

返回值为 const 的函数

  • 将函数的返回值声明为 const 可以防止对返回值进行修改,通常用于返回类成员的引用时。
const int& getValue()const {
    return value;
}

constmutable 结合

  • 使用 mutable 关键字可以让某些成员变量在 const 成员函数中被修改。
class MyClass {
public:
    void increment()const { count++; } // 可以在 const 成员函数中修改 count
private:
    mutable int count = 0; // mutable 允许在 const 函数中修改
};
  • mutable 关键字让成员变量在 const 成员函数中可以被修改,适用于缓存、计数等场景。
  • mutable 仅用于类的非静态成员变量,且仅在 const 成员函数中生效。

单例模式

单例模式确保一个类只有一个实例,并提供全局访问点

单例模式通常用在数据库连接池、线程池等应用场景

关键之处:

  • 私有化的构造函数
  • 提供一个静态成员函数返回静态对象实例,用于访问唯一实例
class single{
public:
    static single& getsingle(){
        static single instance;
        return instance;
    }
private:
    Single(){}

饿汉式单例

线程安全

class Singleton {
private:
    Singleton() {} // 私有构造函数
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;

    static Singleton instance; // 静态实例
public:
    static Singleton& getInstance() {
        return instance;
    }
};

懒汉式单例

需要时才创建,线程不安全,可能多个线程同时创建单例

class Singleton {
private:
    static Singleton* instance;
    Singleton() {}

public:
    static Singleton* getInstance() {
        if(instance == nullptr) {
            instance = new Singleton();
        }
        return instance;
    }
};

线程安全的懒汉式单例

通过互斥锁来保证线程安全

#include <mutex>

class Singleton {
private:
    static Singleton* instance;
    static std::mutex mtx;
    Singleton() {}

public:
    static Singleton* getInstance() {
        std::lock_guard<std::mutex> lock(mtx);
        if(instance == nullptr) {
            instance = new Singleton();
        }
        return instance;
    }
};

每次调用 getInstance 都要加锁,影响性能。

双重检查锁的懒汉式单例(推荐)

#include <mutex>

class Singleton {
private:
    static Singleton* instance;
    static std::mutex mtx;
    Singleton() {}

public:
    static Singleton* getInstance() {
        if(instance == nullptr) {
            std::lock_guard<std::mutex> lock(mtx);
            if(instance == nullptr) {
                instance = new Singleton();
            }
        }
        return instance;
    }
};

// 静态成员初始化
Singleton* Singleton::instance = nullptr;
std::mutex Singleton::mtx;

在多线程环境下效率较高,先检查指针,再加锁,再次检查,确保只创建一次实例。

这种方式采用双锁机制,安全且在多线程情况下能保持高性能。

指针和引用

引用本质上是别名,在内存中与被引用变量占用同一存储单元

引用不能为空,必须在定义时初始化

引用一旦初始化不能再改变指向,但能改变变量值

对引用 sizeof,返回的是被引用变量的大小

智能指针

1731596119562-317818d7-9b3f-4ba4-8ffb-93729da282ad.png

友元函数

友元函数是 C++ 中的一种特殊函数,它能够访问类的私有(**private**)和受保护(**protected**)成员,而普通函数不能直接访问这些成员。友元函数并不是类的成员函数,但它与类关系密切,通过 **friend**** 关键字声明。**

就像朋友一样

当两个类需要紧密协作时,友元函数允许其中一个类直接操作另一个类的内部数据。跨类访问私有成员

如:

class Box {
private:
    double width;
public:
    Box(double w) :width(w) {}

    // 声明友元函数
    friend void printWidth(const Box& b);
};

// 定义友元函数
void printWidth(const Box& b) {
    cout << "Width: " << b.width << endl;
}

int main() {
    Box b(10.5);
    printWidth(b);  // 直接访问私有成员 width
    return 0;
}
//输出
Width:10.5

**跨类访问私有成员: **当两个类需要紧密协作时,友元函数可以实现跨类访问,避免暴露内部实现。

class Square;

class Rectangle {
private:
    double length,width;
public:
    Rectangle(double l,double w) :length(l),width(w) {}

    // 声明友元函数
    friend double areaDifference(const Rectangle&,const Square&);
};

class Square {
private:
    double side;
public:
    Square(double s) :side(s) {}

    // 声明友元函数
    friend double areaDifference(const Rectangle&,const Square&);
};

// 友元函数定义
double areaDifference(const Rectangle& rect,const Square& sq) {
    double rectArea = rect.length * rect.width;
    double squareArea = sq.side * sq.side;
    return rectArea - squareArea;
}

int main() {
    Rectangle rect(4.0,5.0);
    Square sq(3.0);

    cout << "Area difference: " << areaDifference(rect,sq) << endl;

    return 0;
}
//输出
// Area difference:11

Q1. 为什么需要友元函数?

友元函数可以让非成员函数直接访问类的私有和受保护成员,用于:

  1. 实现多个类之间的协作。
  2. 重载非成员运算符(如 <<>>)。
  3. 避免暴露类的实现细节(如不将某些成员设置为 public)。

Q2. 友元函数是否破坏封装性?

  • :友元函数能够直接访问类的私有成员,这似乎违背了封装的原则。
  • :友元函数是被类显式声明的,访问权限是受控的,因此它是一种受限的访问机制,不会完全破坏封装。

Q3. 友元函数和成员函数的区别?

特性 友元函数 成员函数
定义位置 类外定义,但需在类内声明 类内声明和定义(也可类外定义)
访问权限 可以访问私有和受保护成员 可以访问私有和受保护成员
调用方式 像普通函数一样调用 必须通过对象或指针调用
是否属于类 不属于类 属于类

Q4. 友元函数是否可以继承?

  • 友元关系不能继承
  • 子类不会自动继承父类的友元函数,需要在子类中重新声明。

Q5. 友元函数和友元类的区别?

特性 友元函数 友元类
定义 一个函数 一个类
访问范围 访问类的私有和受保护成员 访问类的所有私有和受保护成员
使用场景 用于具体的函数协作 用于复杂类间协作

与对象和类有关的知识点

  • 封装:通过隐藏数据实现信息保护,提供友好的接口。
  • 继承:实现代码复用,并通过父类的扩展定义子类。
  • 多态:通过虚函数实现动态绑定,允许子类重写父类的方法,使得程序更灵活。

继承

类的继承方式:

  • **public**** 继承**:父类的 public 成员在子类中保持为 publicprotected 成员在子类中保持为 protected
  • **protected**** 继承**:父类的 publicprotected 成员在子类中都变为 protected
  • **private**** 继承**:父类的 publicprotected 成员在子类中都变为 private

继承的父类成员:

  • 私有成员属性和方法(**private**
    • 私有成员会被子类继承,但子类无法直接访问或调用这些成员。
    • 私有成员只能通过父类提供的公共或受保护方法(publicprotected)间接访问。
  • 受保护成员(**protected**
    • 受保护成员会被子类继承,且子类可以直接访问。
  • 公共成员(**public**
    • 公共成员会被子类继承,且子类可以直接访问。

private 成员的目的是隐藏实现细节,确保它只能被类自身访问。子类不能直接访问私有成员的原因是为了更好地封装和安全性。

构造函数、析构函数、友元函数、静态数据成员、静态成员函数都不能被继承!

protected 关键字

  • 保护成员的访问权限
    • protected 成员在类的外部不可访问。即不能通过对象.成员的方式访问
    • 但它可以被**该类的派生类(子类)**直接访问。
  • 继承中的使用
    • 在继承中,protected 成员会根据继承方式(publicprotectedprivate)调整其在派生类中的访问权限。

多态

C++ 的多态分为编译时多态(函数重载、运算符重载)和运行时多态(虚函数实现的动态多态)。

运行时多态的特点

  • 使用虚函数virtual)实现。
  • 父类中的虚函数可以在子类中被重写(重载)。
  • 使用基类指针或引用调用虚函数时,调用的是子类的实现(动态绑定)。

什么是动态绑定:

基类指针指向子类对象时,如 Animal* ani = new Duck();调用的成员方法在运行时动态绑定到子类中的重写方法。若不使用 virtual 实现多态(虚函数),而是在子类中定义相同的方法,则不能动态绑定。

class Animal {
public:
    void jiao(){ cout << "动物叫" << endl;}

};

class Duck: public Animal {
public:
    void jiao()  { cout << "嘎嘎嘎" << endl;}
};

int main(){
    Duck duck;
    Animal* ani = new Duck();
    duck.jiao();
    ani->jiao();
}
//输出:
嘎嘎嘎
动物叫
class Animal {
public:
    virtual void jiao(){ cout << "动物叫" << endl;}

};

class Duck: public Animal {
public:
    void jiao()override { cout << "嘎嘎嘎" << endl;}
};

int main(){
    Duck duck;
    Animal* ani = new Duck();
    duck.jiao();
    ani->jiao();
}
//输出:
嘎嘎嘎
嘎嘎嘎