C++基础

Channing Hsu

一、访问权限:

C++通过public、protected、private三个关键字来控制成员变量和成员函数的访问权限。

1. public 访问权限:

  • 在类的内部和外部都可以访问。
  • 对外提供接口,表示该成员对外可见。
1
2
3
4
5
6
7
class Example {
public:
int publicVar; // 公有成员变量
void publicFunction() {
// 公有成员函数
}
};

2. protected 访问权限:

  • 在类的内部可以访问,派生类中也可以访问,但在类的外部不能访问。
  • 主要用于继承关系,表示该成员对派生类可见。
1
2
3
4
5
6
7
class Example {
protected:
int protectedVar; // 受保护成员变量
void protectedFunction() {
// 受保护成员函数
}
};

3. private 访问权限:

  • 只有在类的内部可以访问,外部不能访问,包括派生类中也不能直接访问。
  • 主要用于隐藏实现细节,表示该成员对外不可见。
1
2
3
4
5
6
7
class Example {
private:
int privateVar; // 私有成员变量
void privateFunction() {
// 私有成员函数
}
};

4. 访问权限与继承:

  • 公有继承(public inheritance): 基类中的公有成员和保护成员都是派生类可见的,私有成员在派生类中不可见。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    class Base {
    public:
    int publicVar;
    protected:
    int protectedVar;
    private:
    int privateVar;
    };

    class Derived : public Base {
    // Derived 类中可以访问 publicVar 和 protectedVar,但不能访问 privateVar
    };
  • 私有继承(private inheritance): 基类中的公有成员和保护成员都成为派生类的私有成员,不可见。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    class Base {
    public:
    int publicVar;
    protected:
    int protectedVar;
    private:
    int privateVar;
    };

    class Derived : private Base {
    // Derived 类中无法直接访问 Base 的任何成员
    };
  • 受保护继承(protected inheritance): 基类中的公有成员和保护成员都成为派生类的保护成员,不可见。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    class Base {
    public:
    int publicVar;
    protected:
    int protectedVar;
    private:
    int privateVar;
    };

    class Derived : protected Base {
    // Derived 类中可以访问 publicVar 和 protectedVar,但不能访问 privateVar
    };

二、C++ 三大特性

1. 继承:

  • 定义: 继承是一种面向对象编程的机制,允许一个类(子类/派生类)基于另一个类(父类/基类)的定义构建。子类继承了父类的属性和方法,并可以在不重新编写原来的类的情况下扩展或修改这些功能。
  • 功能:
    • 实现继承: 子类获得基类的属性和方法的能力,无需额外编码。
    • 接口继承: 子类使用属性和方法的名称,但必须提供实现。
    • 可视继承: 子类使用基类的外观和实现代码的能力。
  • 示例:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    // 基类 - 人
    class Person {
    public:
    std::string name;
    int age;

    void eat() {
    std::cout << name << " is eating." << std::endl;
    }

    void sleep() {
    std::cout << name << " is sleeping." << std::endl;
    }
    };

    // 派生类 - 学生
    class Student : public Person {
    public:
    void study() {
    std::cout << name << " is studying." << std::endl;
    }
    };

2. 封装:

  • 定义: 封装是将数据和代码捆绑在一起,避免外界直接访问和修改对象的内部状态。通过将数据成员声明为私有的,通过公共方法提供对数据的访问和修改。
  • 功能: 封装提供了信息隐藏的机制,使得对象的内部实现对外部不可见,提高了程序的安全性和可维护性。
  • 示例:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    // 封装示例
    class BankAccount {
    private:
    double balance;

    public:
    // 公共方法提供对私有成员的访问
    void deposit(double amount) {
    if (amount > 0) {
    balance += amount;
    }
    }

    double getBalance() const {
    return balance;
    }
    };

3. 多态:

  • 定义: 多态性允许对象在运行时表现出不同的行为。在C++中,多态性通过虚函数继承实现。可以在基类中声明虚函数,在派生类中重新定义该虚函数,以便在运行时调用正确的函数。
  • 功能: 多态性提供了一种机制,可以根据对象的实际类型来选择调用相应的方法,而不是在编译时确定。
  • 实现方式:
    • 覆盖(Override): 子类重新定义基类的虚函数。
    • 重载(Overload): 允许存在多个同名函数,但参数表不同。
  • 示例:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    // 基类 - 动物
    class Animal {
    public:
    virtual void makeSound() const {
    std::cout << "Animal makes a sound." << std::endl;
    }
    };

    // 派生类 - 狗
    class Dog : public Animal {
    public:
    void makeSound() const override {
    std::cout << "Dog barks." << std::endl;
    }
    };

    // 派生类 - 猫
    class Cat : public Animal {
    public:
    void makeSound() const override {
    std::cout << "Cat meows." << std::endl;
    }
    };

这些概念一起构成了面向对象编程的三大特征:继承、封装和多态。它们帮助提高代码的可维护性、可扩展性和重用性。

三、数据类型

1. 整型数据类型

  • short: 至少16位。通常为2字节。
  • int: 至少与 short 一样长。通常为4字节。
  • long: 至少32位,且至少与 int 一样长。通常为4字节。
  • long long: 至少64位,且至少与 long 一样长。通常为8字节。

2. C++ 整型数据长度标准

  • 1 byte = 8 bit: 在大多数系统中,1字节等于8位。

  • 系统最小长度: 很多系统使用最小长度,例如:

    • short 通常为16位(2字节)。
    • long 通常为32位(4字节)。
    • long long 通常为64位(8字节)。
    • int 长度较为灵活,通常被认为为4字节,与 long 等长。

3. sizeof 运算符

  • sizeof 运算符用于获取数据类型或对象的大小(以字节为单位)。
  • 通过以下示例,可以显示不同数据类型的大小:
    1
    2
    cout << "int is " << sizeof(int) << " bytes. \n";
    cout << "short is " << sizeof(short) << " bytes. \n";

4. climits 头文件

  • climits 头文件定义了一些整型数据类型的符号常量,如:
    • INT_MAX:表示 int 的最大值。
    • INT_MIN:表示 int 的最小值。

5. 无符号类型

  • 无符号整型 (unsigned) 不存储负数值,可以增大变量能够存储的最大值,但数据长度不变。
  • 例如,unsigned int 可以存储从0到2^32 - 1的非负整数。

选择数据类型

  • int 被设置为自然长度,即计算机处理效率最高的长度,因此在选择数据类型时,通常选择 int 类型。

这些规定和概念有助于程序员理解和选择适当的整型数据类型,以满足其需求并保证代码的可移植性。

四、指针与引用

指针与引用是C++中两种不同的概念,它们分别用于处理内存地址和创建变量别名。

1. 指针(Pointer)

  1. 存放对象地址: 指针是一个变量,其存放的是某个对象的内存地址。
  2. 可变性: 指针本身可以改变,即可以指向不同的内存地址。包括指向的地址的改变和指向的地址中所存放的数据的改变。
  3. 指向指针的指针: 指针本身也有地址,因此可以有指向指针的指针。
  4. 需要初始化: 在使用指针之前,需要先初始化,通常通过将其指向一个对象的地址来实现。
1
2
3
int x = 10;
int *ptr = &x; // ptr是一个指向int类型的指针,存放x的地址
int **ptr_to_ptr = &ptr; // 指向指针的指针
  • int *ptr: 定义了一个指向整数的指针。
  • &x: 取地址运算符,返回变量x的地址。

2. 引用(Reference)

  1. 变量的别名: 引用是变量的别名,它引用(代表)了一个已存在的对象。
  2. 不可变性: 一旦引用被初始化,它将一直引用同一个对象,不可变。引用必须在初始化时指定引用的对象,之后不能更改。
  3. 不存在空引用: 引用不存在指向空值的情况,它必须引用一个已存在的对象。
1
2
int x = 10;
int &ref = x; // ref 是 x 的引用
  • int &ref: 定义了一个整数的引用。
  • x: 引用的是变量x

3. 解引用(Dereference):

解引用是通过指针或引用访问其指向的内存地址中存储的值。

1
2
3
int x = 10;
int *ptr = &x;
int y = *ptr; // 解引用ptr,将x的值赋给y
  • *ptr: 解引用指针ptr,得到存储在ptr指向的内存地址中的值。

4. 取址符(Address-of Operator):

取址符(&)用于获取变量的内存地址。

1
2
3
cppCopy code
int x = 10;
int *ptr = &x; // ptr存放x的地址
  • &x: 取得变量x的地址,将其赋给指针ptr

5. 指针与引用的比较

指针的特性

  • 可以为空:指针可以指向 nullptr,表示它不指向任何对象。
  • 可以重新赋值:指针可以在其生命周期内指向不同的对象。
  • 支持算术运算:指针可以进行算术运算,如递增、递减等。
  • 需要解引用:访问指针所指向的对象时需要使用 * 操作符。
  • 内存占用:指针本身占用内存(通常为4或8字节,取决于系统架构)。

引用的特性

  • 不能为空:引用必须在声明时初始化,且不能为 nullptr
  • 不可重新赋值:引用在其生命周期内始终引用同一个对象。
  • 不支持算术运算:引用不能进行指针算术运算。
  • 无需解引用:引用直接访问对象,无需使用 * 操作符。
  • 内存占用:引用本身不占用额外内存,只是原变量的一个别名。
1
2
3
4
5
6
int x = 10;
int *ptr = &x;
int &ref = x;

*ptr = 20; // 修改 x 的值
// ref = 20; // 此行会直接修改 x 的值

五、关键字

1. const 关键字

const 是 C++ 中用于声明常量的关键字,它可以用于不同的上下文中,限定变量、指针、引用等的特性。

1. const 修饰变量(常量):

1
2
const int max_value = 100;  // 定义一个常量 max_value
const double pi = 3.14159; // 定义一个常量 pi

在这里,max_valuepi 都是常量,它们的值不能被修改。

2. const 修饰指针:

  • 底层 const(常量指针):

    定义:具有只能够读取内存中数据,却不能够修改内存中数据的属性的指针,称为指向常量的指针,简称常量指针。

    1
    2
    3
    4
    int temp = 10;
    // a是一个常量指针,它指向一个只读的对象,不能通过a修改temp的值。
    const int* a = &temp;
    int const* a = &temp;
  • 顶层 const(指针常量):

    定义:指针常量是指指针所指向的位置不能改变,即指针本身是一个常量,但是指针所指向的内容可以改变。

    注:指针常量必须在声明的同时对其初始化,不允许先声明一个指针常量随后再对其赋值,这和声明一般的常量是一样的。

    1
    2
    3
    4
    int temp = 10;
    int temp2 = 20;
    // p是一个指针常量,它的值只能在初始化时指向temp,其他地方不能改变。即p本身不能指向其他地址。
    int* const p = &temp;

注意:

  • const 关键字的位置决定是底层 const 还是顶层 const。
  • const 修饰指针时,要根据 const 的位置分清是底层 const 还是顶层 const。

2. Define 和 Typedef 、Inline的区别:

Define:

  • 定义方式: 使用 #define 关键字,将标识符与一个表达式或者语句进行关联。
  • 类型检查: 无类型检查,仅进行简单的字符串替换。
  • 作用范围: 在预处理阶段进行替换,是一种文本替换机制。
  • 常用场景: 常用于定义宏,进行代码中的文本替换,如宏定义常量、条件编译等。
  • 使用灵活性: 没有对应的数据类型,可以定义任何文本。
1
2
#define PI 3.14
#define MAX(a, b) ((a) > (b) ? (a) : (b))

Typedef:

  • 定义方式: 使用 typedef 关键字,为一个已有的数据类型取一个新的名字。
  • 类型检查: 提供类型检查,用于定义新的类型别名。
  • 作用范围: 在编译和运行时生效,具有对应数据类型。
  • 常用场景: 用于提高代码的可读性,为复杂的类型起一个更简洁的名字。
  • 使用灵活性: 可以对各种类型进行别名定义。
1
2
typedef int Integer; // Integer 是 int 的别名
typedef void (*FunctionPointer)(); // FunctionPointer 是函数指针类型的别名

Inline:

inline是先将内联函数编译完成生成了函数体直接插入被调用的地方,减少了压栈,跳转和返回的操作。没有普通函数调用时的额外开销;

  • 定义方式: 使用 inline 关键字,对函数进行内联展开。
  • 类型检查: 提供类型检查,是真正的函数调用。
  • 编译时机: 在编译时进行内联展开,替代函数调用。
  • 常用场景: 适用于函数体简单、频繁调用的场景,提高执行效率。
1
2
3
inline int square(int x) {
return x * x;
}

C++中inline编译限制:

  1. 不能存在任何形式的循环语句
  2. 不能存在过多的条件判断语句
  3. 函数体不能过于庞大
  4. 内联函数声明必须在调用语句之前

总体而言,typedef 用于创建类型别名,提高代码的可读性和可维护性;define 用于进行简单的文本替换,例如宏定义;inline 用于函数的内联展开,提高执行效率。每个关键字在不同的场景下都有其独特的作用。

3. Override 和Overload

Override(重写):

  1. 定义: 在子类中定义一个与父类中具有相同名称和参数列表的方法,以实现不同的功能。

  2. 规则:

    • 重写方法的参数列表、返回值、所抛出的异常应与被重写方法一致。
    • 被重写的方法不能为 private
    • 静态方法不能被重写为非静态的方法。
    • 重写方法的访问修饰符一定要大于等于被重写方法的访问修饰符(public > protected > default > private)。
  3. 使用场景: 主要用于子类继承父类时,根据需要重新实现父类的方法。

Overload(重载):

  1. 定义: 在同一个类中,可以有多个同名的方法,但它们的参数列表不同,以实现不同的功能。

  2. 规则:

    • 不能通过访问权限、返回类型、抛出的异常进行重载。
    • 不同的参数类型可以是不同的参数类型、不同的参数个数、不同的参数顺序(参数类型必须不一样)。
    • 方法的异常类型和数目不会对重载造成影响。
  3. 使用场景: 主要用于一个类中,为同一方法提供不同的版本,以适应不同的参数情况。

本质区别:

  • 加入了 override 修饰符的方法,在使用时始终只有一个被使用。这是因为 override 用于明确表示这是对父类方法的重写,编译器会在编译时检查确保符合规则。而重载则是在同一类中,可以存在多个同名方法,根据不同的参数调用不同的版本。

使用多态:

  • 使用重写和重载的目的是为了实现多态性,提高代码的灵活性和可维护性。多态性允许以一种统一的方式对待不同的对象,通过基类指针或引用调用相同的方法,实现对不同派生类对象的统一操作。

重新生成一下解释,并提供一个小例子:

4. new和 malloc

  1. 内存分配失败时的处理:

    • newnew无法分配所需的内存时,会抛出std::bad_alloc异常。
    • mallocmalloc无法分配所需的内存时,会返回NULL
  2. 指定内存块大小:

    • new 无需显式指定内存块的大小,系统会自动计算。
    • malloc 需要显式指定所需内存的大小。
  3. 重载:

    • new operator newoperator delete 可以被重载。
    • malloc mallocfree 不能被直接重载。
  4. 构造函数和析构函数的调用:

    • new new 操作符会调用对象的构造函数,delete 操作符会调用对象的析构函数。
    • malloc mallocfree 不会调用对象的构造和析构函数。
  5. 标准库和语言:

    • new C++运算符,用于动态内存管理。
    • malloc C语言标准库函数,在C++中也可用。
  6. 内存分配来源:

    • new 从自由存储区(free store)上为对象动态分配内存空间。
    • malloc 从堆(heap)上动态分配内存。

image-20231227150117529

5. constexprconst

const(常量):

  1. 语义: 表示“只读”的语义,用于声明不可修改的变量。

  2. 运行期和编译期常量: 可以定义编译期常量,也可以定义运行期常量。

  3. 成员函数和变量: 将成员函数标记为 const 表示该函数不会修改对象的状态;将变量标记为 const 表示该变量的值不可修改。

  4. 指针和 const

    • 指针常量: const int* d = new int(2); 表示指向常量的指针。
    • 常量指针: int *const e = new int(2); 表示常量指针,指针本身是常量。
  5. const 的修改: 若要修改 const 修饰的变量的值,需要加上关键字 volatile

  6. const 成员函数和 mutable

    • const 成员函数:表示该函数不会修改对象的状态。
    • mutable 关键字:用于修饰某些与类状态无关的数据成员,允许在 const 成员函数中修改这些成员。

constexpr(常量表达式):

  1. 语义: 表示“常量”的语义,用于定义编译期常量。

  2. 常量初始化: 必须使用常量初始化,例如:

    1
    2
    3
    constexpr int n = 20;
    constexpr int m = n + 1;
    static constexpr int MOD = 1000000007;
  3. 指针和 constexpr 如果 constexpr 声明中定义了一个指针,仅对指针有效,和所指对象无关,例如:

    1
    constexpr int *p = nullptr; // 常量指针(顶层 const)

    constexpr关键字意味着p在编译时是一个常量,这在C++11及更高版本中被引入,用于编译时常量表达式。对于指针来说,constexpr指针意味着指针本身是一个常量,且必须初始化。constexpr指针通常用于在编译时需要已知值的场景,如数组大小、模板参数等。

    1
    const int *q = nullptr;     // 指向常量的指针(底层 const)

    const关键字在指针声明中的位置不同,会有不同的含义。对于const int *qconst修饰的是int,表示指向常量的指针。这种指针用于需要保护数据不被修改的场景,常见于接口参数,确保函数不修改传入的数据。

    1
    int *const r = nullptr;     // 顶层 const 常量指针

    const关键字放在指针声明的后面,修饰的是指针本身,表示指针是常量,但指针指向的值可以改变。常量指针用于保证指针本身的地址不变,但允许修改指针指向的数据。常用于需要固定地址的指针,如引用某个固定的缓冲区。

  4. constexpr 函数:

    • 用于常量表达式的函数,返回类型和所有形参类型都是字面值类型。
    • 函数体有且只有一条 return 语句。
    • 为了在编译过程展开,constexpr 函数被隐式转换成了内联函数。
  5. constexpr 构造函数:

    • 构造函数不能被声明为 const,但字面值常量类的构造函数可以是 constexpr
    • 必须有一个空的函数体,所有成员变量的初始化都放到初始化列表中。
    • 对象调用的成员函数必须使用 constexpr 修饰。

constconstexpr 的区别:

  1. const 表示只读,可以定义运行期常量,而 constexpr 表示常量,只能定义编译期常量。

  2. constexpr 变量在复杂系统中可以用于明确表达初始值是常量表达式,由编译器验证变量的值是否是常量表达式。

  3. constexpr 函数被隐式转换成内联函数,可以在编译过程中展开,提高效率。相比宏,constexpr 更安全可靠,并且没有额外的开销。

  4. 如果将函数或变量标记为 constexpr,它同样是 const 的,但相反并不成立。一个 const 的变量或函数并不一定是 constexpr 的。

6. volatile:

定义:

volatile 是一个类型修饰符,与 const 绝对对立。它影响编译器对变量的编译结果,用于声明的变量表示该变量随时可能发生变化,与该变量有关的运算不要进行编译优化。使用 volatile 关键字声明的变量会导致每次访问时都从内存中重新加载内容,而不是直接从寄存器拷贝内容。

作用:

  • 保证本条指令不会因编译器的优化而省略。
  • 要求每次直接读取值,确保对特殊地址的稳定访问。

使用场合:

  • 在中断服务程序和与 CPU 相关寄存器的定义,以确保对这些变量的访问不会受到编译器的优化影响。

举例说明:

编译器优化是为了生成更高效的代码,减少程序的运行时间和资源消耗。当编译器发现一个空循环,即循环体中没有任何对程序有实际影响的操作时,它会认为这个循环没有任何意义,因此可能会将其优化掉,以提高程序的效率。

volatile 告诉编译器,这个变量可能会在程序的其他地方以不可预测的方式被修改,因此每次访问都必须实际读取内存,从而防止优化。

1
2
// 示例:空循环,使用 volatile 防止被优化掉
for (volatile int i = 0; i < 100000; i++);

7. extern:

定义:

extern 用于声明外部变量,即在函数或文件外部定义的全局变量。

8. 静态变量、全局变量和局部变量:

静态变量(Static Variable)

  • 定义:在函数或类中使用 static 关键字声明的变量。
  • 生命周期:从程序开始到程序结束。
  • 作用域:如果在函数内声明,作用域是函数内;如果在类中声明,作用域是类的实例。
  • 内存分布:存储在数据段(Data Segment)。
  • 特点
    • 在函数中声明的静态变量在函数调用结束后不会销毁,下次调用时保持上次的值。
    • 在类中声明的静态成员变量是所有对象共享的。
静态局部变量
1
2
3
4
5
void functionWithStatic() {
static int localVar = 0; // 仅第一次调用时初始化
localVar++;
// 每次调用该函数时,localVar的值会保留
}
静态全局变量
1
2
3
4
5
6
// file1.cpp
static int staticGlobalVar = 10;

void someFunction() {
staticGlobalVar = 20; // 仅在file1.cpp中可见和修改
}

全局变量(Global Variable)

  • 定义:在所有函数外部声明的变量。
  • 生命周期:从程序开始到程序结束。
  • 作用域:整个程序(所有文件),但可以用 static 限制其作用域在声明的文件内。
  • 内存分布:存储在数据段(Data Segment)。
  • 特点
    • 任何函数都可以访问和修改全局变量。
    • 易引起命名冲突和难以维护。
1
2
3
4
5
6
7
8
// file1.cpp
int globalVar = 10;

// file2.cpp
extern int globalVar;
void someFunction() {
globalVar = 20; // 修改全局变量
}

局部变量(Local Variable)

  • 定义:在函数或块内声明的变量。
  • 生命周期:从声明开始到所在的块或函数结束。
  • 作用域:声明所在的块或函数。
  • 内存分布:存储在栈区(Stack)。
  • 特点
    • 只能在声明的块或函数内访问。
    • 使用局部变量不会污染全局命名空间。
1
2
3
4
5
6
7
8
9
10
11
#include <iostream>
void demoFunction() {
int localVar = 5;
std::cout << "LocalVar: " << localVar << std::endl;
}
int main() {
demoFunction();
// std::cout << localVar << std::endl; // 错误,localVar在此不可见
return 0;
}

static 关键字作用:

  1. 静态变量(函数内部)

  2. 静态变量(类内部)

  3. 静态函数

    • 只能访问类的静态成员变量。
    • 不能访问类的非静态成员变量。
    1
    2
    3
    4
    5
    6
    class Example {
    public:
    static void func() {
    // 静态函数只能访问静态成员变量
    }
    };
  4. 静态全局变量

    • 作用域限制在声明的文件内部。
    • 其他文件不能直接访问。
    1
    static int globalVar = 10;

const 关键字作用:

1. 常量变量

定义:用 const 修饰的变量,值不能修改。

示例

1
2
const int a = 10;
// a = 20; // 错误:不能修改常量变量的值

2. 常量指针(Pointer to const)

定义:指针指向的是常量数据,不能通过指针修改数据。

示例

1
2
3
4
int value = 10;
const int* ptr = &value;
// *ptr = 20; // 错误:不能通过 ptr 修改 value 的值
ptr = &anotherValue; // 正确:可以改变 ptr 的指向

3. 指针常量(Const pointer)

定义:指针本身是常量,即指针所指向的地址不可变。

示例

1
2
3
4
int value = 10;
int* const ptr = &value;
*ptr = 20; // 正确:可以通过 ptr 修改 value 的值
// ptr = &anotherValue; // 错误:不能改变 ptr 的指向

4. 常量成员函数

定义:表示函数不修改类的成员变量。

示例

1
2
3
4
5
6
7
8
class Example {
public:
void func() const {
// 成员函数不能修改类的成员变量
}
private:
int value;
};

5. 常量引用(Reference to const)

定义:不能通过引用修改引用的对象。

示例

1
2
3
int value = 10;
const int& ref = value;
// ref = 20; // 错误:不能通过引用修改 value 的值

6. 常量参数

定义:用于函数参数,防止函数内部修改传入的参数值。

示例

1
2
3
void func(const int param) {
// param = 20; // 错误:不能修改常量参数的值
}

7. 返回值常量

定义:用于函数返回值,防止调用者修改返回的对象。

示例

1
2
3
4
5
6
7
const int getValue() {
return 10;
}

// 调用者不能修改返回值
int value = getValue();
// value = 20; // 错误:不能修改常量返回值

const 关键字的主要作用是保护数据不被意外修改,提高代码的安全性和可读性。根据应用场景,const 可以修饰变量、指针、引用、成员函数、参数等。通过合理使用 const,可以确保代码中某些值的不可变性,从而减少潜在的错误。

区别与使用场景

  • static 用于控制变量的生命周期、作用域以及类成员的共享性。
  • const 用于防止变量或对象被修改,增强代码的安全性和稳定性。

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include <iostream>

class MyClass {
public:
static int staticVar; // 静态成员变量
const int constVar; // 常量成员变量

MyClass(int val) : constVar(val) {}

static void staticFunc() {
// 静态成员函数
}

void constFunc() const {
// 常量成员函数
}
};

int MyClass::staticVar = 0;

int main() {
static int staticVar = 10; // 静态局部变量
const int constVar = 20; // 常量变量

MyClass obj(30);
obj.constFunc();
MyClass::staticFunc();

return 0;
}

9. 前置++与后置++

在 C++ 中,++ 运算符可以被重载以支持前置和后置递增操作。

举例说明

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
#include <iostream>

// 定义链表节点
class ListNode {
public:
ListNode* next;
// 可以添加其他成员...
};

// 定义链表迭代器
class ListIterator {
public:
typedef ListIterator self;
typedef ListNode* linktype;

ListIterator(linktype node) : node(node) {}

// 前置递增运算符重载
self& operator++() {
node = node->next; // 移动到下一个节点
return *this;
}

// 后置递增运算符重载
const self operator++(int) {
self tmp = *this; // 保存当前状态
++(*this); // 调用前置递增运算符
return tmp; // 返回保存的临时对象
}

// 其他成员函数...

// 获取当前节点指向的对象(假设为 int)
int getValue() const {
return node ? node->value : 0; // 假设 ListNode 有一个 value 成员
}

private:
linktype node;
};

int main() {
ListNode node1, node2, node3;
node1.next = &node2;
node2.next = &node3;
node3.next = nullptr;

ListIterator it(&node1); // 创建迭代器,指向链表的第一个节点

// 使用前置递增运算符遍历链表并打印值
std::cout << "Traversal using prefix increment:" << std::endl;
while (it.getValue() != 0) {
std::cout << it.getValue() << " ";
++it;
}
std::cout << std::endl;

// 使用后置递增运算符遍历链表并打印值
ListIterator it_postfix = ListIterator(&node1);
std::cout << "Traversal using postfix increment:" << std::endl;
while (it_postfix.getValue() != 0) {
std::cout << it_postfix.getValue() << " ";
it_postfix++;
}
std::cout << std::endl;

return 0;
}
  • 返回对象 const self,表示后置++会返回原对象的值,并在原对象上执行递增操作。
  • 参数 int 是用于区分前后置++的占位参数,编译器在调用时为其提供一个默认值(通常是0)。
  1. 为什么后置++返回对象而不是引用?

    • 后置++需要返回原对象的值,但为了保持语言的一致性,也就是为了与内置类型的行为保持一致,返回一个值而不是引用。这是为了防止在后置++中的连续调用中产生不可预测的结果,因为后置++首先返回原值,然后再进行递增。
  2. 为什么后置++前面要加 const?

    • 在内置类型中,后置++返回一个值,而不是引用,这与内置类型的行为是一致的。为了与内置类型行为一致,防止用户连续两次调用后置++,比如 i++++,在第一次后置++后返回的是旧值,而不是原对象,所以为了防止连续调用,通过在后置++函数前面加上 const 可以禁止其合法化。
  3. 处理用户的自定义类型为什么最好使用前置++?

    • 前置++不会创建临时对象,直接在原对象上进行递增操作,避免了构造和析构带来的额外开销。在性能要求较高的情况下,使用前置++更为高效。

在C++中,a++int a = b 这两种操作在多线程环境下并不是线程安全的。原因在于这些操作涉及到对变量的读取和修改,而在多线程环境下,多个线程可能同时对同一变量进行读写操作,从而导致竞态条件和数据不一致的问题。

10. std::atomic

std::atomic 是 C++11 标准引入的一个模板类,用于实现多线程并发编程中的原子操作。它提供了一种线程安全的方式来操作共享的内存变量,避免了竞态条件(race condition)和数据竞争(data race)的问题。

  • 原子操作atomic 类模板提供了多种原子操作,包括加载、存储、交换、比较和交换等,这些操作能够在不需要额外的同步机制(如互斥锁)的情况下,确保对共享变量的安全访问。

  • 数据类型atomic 可以用于支持标量类型(如整数、浮点数、指针等),也可以用于支持某些特定类型的自定义类型(通过特化 atomic 模板)。

  • 内存模型atomic 保证了操作的原子性,即这些操作在执行时不会被中断,其他线程也不会在这些操作中间修改同一变量,从而避免了竞态条件和数据竞争。

  • 线程安全:使用 atomic 可以避免手动编写复杂的同步代码,提高了编程的简洁性和可读性,同时提升了多线程程序的性能。

11. 问题:a++和 int = a + b是否是线程安全的?

a++

对于 a++ 操作,它实际上包含了读取、递增和写回三个步骤。即使从语法层面看,这是一个语句,应该是原子的,但是在底层的汇编指令中,这三个步骤并不是原子执行的。以下是可能的汇编指令:

1
2
3
mov eax, dword ptr [a]  // 读取a的值
inc eax // 将寄存器中的值递增
mov dword ptr [a], eax // 写回a的值

在多线程环境下,两个线程同时执行 a++ 操作可能会导致竞态条件,因为两个线程读取并递增的时候可能会相互干扰,最终结果可能不是期望的。

int a = b

对于 int a = b 操作,它涉及到将变量 b 的值读取到一个寄存器中,然后将寄存器中的值写入变量 a 所代表的内存地址。这也可能不是原子操作,因为读取和写入是两个步骤。以下是可能的汇编指令:

1
2
mov eax, dword ptr [b]  // 读取b的值到寄存器
mov dword ptr [a], eax // 将寄存器中的值写入a的内存地址

在多线程环境下,两个线程同时执行 int a = b 操作可能会导致一个线程读取 b 的值,而在写回 a 的时候被另一个线程中断,最终结果可能不是期望的。

C++11 引入了 std::atomic,这是一个模板类型,提供了对整数类型的原子操作。使用 std::atomic 可以确保对变量的读写操作是原子的,从而避免了竞态条件和数据不一致的问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
#include <iostream>
#include <thread>
#include <atomic>

std::atomic<int> counter(0);
std::atomic<int> a{5};
std::atomic<int> b{10};
std::atomic<int> result{0};

void calculateResult() {
result = a + b; // 原子相加
}

void incrementCounter() {
for (int i = 0; i < 1000000; ++i) {
counter++; // 原子递增
}
}

int main() {
std::thread t1(incrementCounter);
std::thread t2(incrementCounter);

t1.join();
t2.join();

std::cout << "Final counter value: " << counter << std::endl;

std::thread t3(calculateResult);
std::thread t4(calculateResult);

t3.join();
t4.join();

std::cout << "Final value of result: " << result << std::endl;


return 0;
}

在上面的示例中,std::atomic<int> 类型的 counter 变量被两个线程同时递增,由于使用了原子变量,不需要额外的同步手段,程序能够避免竞态条件。

六、C++强制类型转换

在 C++ 中,有四个强制类型转换关键字:static_castdynamic_castreinterpret_castconst_cast。每个关键字都有其特定的用途和限制。以下是对这四个关键字的详细解释:

1. static_cast

  • 用途: static_cast 主要用于进行静态类型转换。
  • 特点:
    • 没有运行时类型检查,所以在转换的安全性上需要开发者来保证。
    • 可以进行上行转换(派生类指针或引用转换为基类表示),是安全的。
    • 进行下行转换(基类指针或引用转换为派生类表示)是不安全的,因为没有动态类型检查。
  • 使用场景:
    • 用于基本数据类型之间的转换,如将 int 转换为 char
    • 用于将任何类型的表达式转换为 void 类型。
1
2
3
4
5
6
7
8
#include <iostream>
// 这个例子展示了将 double 类型转换为 int 类型的静态转换。static_cast 在这里用于执行基本数据类型之间的转换。
int main() {
double d = 3.14;
int i = static_cast<int>(d); // 静态转换,截断小数部分
std::cout << "Double: " << d << ", Int: " << i << std::endl;
return 0;
}

2. dynamic_cast

  • 用途: dynamic_cast 用于进行动态类型转换,主要用于在继承关系中的类型转换。
  • 特点:
    • 有运行时类型检查,因此对于下行转换更安全。
    • 可以在存在虚函数的父子类关系中进行强制类型转换。
    • 对于指针,转换失败返回 nullptr;对于引用,转换失败会抛出异常。
  • 使用场景:
    • 在进行基类指针或引用到派生类的转换时,提供了安全性保证。
    • 用于类之间的交叉转换,要求基类必须有虚函数。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <iostream>
// 这个例子演示了在继承关系中使用 dynamic_cast 进行类型转换。在这里,Base* 类型指针指向一个 Derived 类型的对象,并使用 dynamic_cast 尝试将其转换为 Derived* 类型。由于有虚函数,这个转换是安全的。
class Base {
public:
virtual ~Base() {}
};

class Derived : public Base {};

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

if (derivedPtr) {
std::cout << "Dynamic Cast Successful." << std::endl;
} else {
std::cout << "Dynamic Cast Failed." << std::endl;
}

delete basePtr;
return 0;
}

3. reinterpret_cast

  • 用途: reinterpret_cast 主要用于进行底层的、不安全的类型转换,通常用于指针和引用的转换。
  • 特点:
    • 允许将整数类型转换为指针,也允许将指针转换为整数类型。
    • 可以在指针和引用之间进行较为自由的转换,但平台移植性较差。
  • 使用场景:
    • 用于进行底层的位操作,例如将指针转换为整数或反之。
1
2
3
4
5
6
7
8
9
10
11
#include <iostream>
// 这个例子展示了将整数类型转换为 void* 类型的使用。reinterpret_cast 主要用于进行低层次的、不安全的类型转换,例如将指针转换为整数。
int main() {
int num = 42;
void* voidPtr = reinterpret_cast<void*>(&num);

std::cout << "Original Int: " << num << std::endl;
std::cout << "Void Pointer: " << voidPtr << std::endl;

return 0;
}

4. const_cast

  • 用途: const_cast 主要用于添加或移除变量的 constvolatile 限定符。
  • 特点:
    • 用于去除类型的 constvolatile 属性。
    • 常量指针可以被转换为非常量指针,常量引用可以被转换为非常量引用。
  • 使用场景:
    • 用于在特定情况下去除常量性,以方便修改变量的值。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <iostream>

int main() {
const int constNum = 10;
int& nonConstRef = const_cast<int&>(constNum);

// 修改非常量引用
nonConstRef = 20;

std::cout << "Original Const Num: " << constNum << std::endl;
std::cout << "Modified Non-Const Ref: " << nonConstRef << std::endl;

return 0;
}

总体而言,这些强制类型转换关键字应该谨慎使用,特别是 reinterpret_cast,因为它在类型转换上非常灵活但也非常危险。在实际使用中,应根据具体场景选择适当的转换方式,同时注意类型安全性和代码的可读性。

运算符重载是 C++ 中的一个重要特性,允许用户自定义类型在运算符上的行为。以下是关于运算符重载的详细解释:

七、重载运算符基本概念

  • 函数 : 重载运算符本质上是函数调用,使得用户定义的类型可以在特定的运算符上像内置类型一样工作。
  • 两种调用方式:
    1. 使用运算符的方式,例如 data1 + data2
    2. 使用函数调用的方式,例如 operator+(data1, data2)

2. 可以重载的运算符

在 C++ 中,有一些运算符是可以被重载的,包括逗号、取地址、逻辑与和逻辑或等。不过,有一些运算符不建议重载,如逗号、取地址等,因为它们本身对类类型有特殊定义,而逻辑与、逻辑或具有短路求值属性,重载可能引起不符合预期的结果。

3. 运算符重载的形式

运算符重载可以是类的成员函数或非成员函数。一些建议如下:

  • 成员函数形式: 对于逻辑上属于某个类的操作,可以使用成员函数形式。例如,对于复数类 Complex,可以使用成员函数形式定义加法运算符。

    1
    2
    3
    4
    5
    6
    class Complex {
    public:
    Complex operator+(const Complex& other) const {
    return Complex(real + other.real, imag + other.imag);
    }
    };
  • 非成员函数形式: 对于逻辑上不属于某个类的操作,可以使用非成员函数形式。例如,对于字符串拼接,可以定义非成员函数形式的加法运算符。

    1
    2
    3
    4
    5
    6
    7
    class String {
    // ...
    };

    String operator+(const String& str1, const String& str2) {
    // 拼接 str1 和 str2
    }

4. 重载运算符的注意事项

  • 参数数量: 重载运算符的参数数量和类型是固定的,对于二元运算符,有两个参数。
  • 成员函数参数: 如果重载运算符是类的成员函数,左侧运算对象相当于固定为 this,右侧运算对象是传递进来的参数。
  • 友元和私有成员: 如果需要访问类的私有成员,可以将非成员函数形式的运算符重载声明为友元函数。

5. 一些建议规则

  • 算术和关系运算符建议非成员: 这些运算符是对称的,建议将它们定义为非成员函数。
  • 赋值运算符必须是成员: 赋值运算符必须修改调用对象的状态,因此必须是成员函数。
  • 下标运算符必须是成员: 下标运算符必须是成员函数,因为它用于访问类对象的元素。
  • 递增递减运算符建议成员: 这些运算符修改对象状态,建议将它们定义为成员函数。

6. 函数调用运算符

函数调用运算符 operator() 允许对象像函数一样被调用。这对于函数对象(例如 Lambda 表达式)非常有用,因为编译器将 Lambda 表达式翻译为一个未命名类的未命名对象。函数调用运算符重载的形式如下:

1
2
3
4
5
6
7
8
9
10
class Functor {
public:
int operator()(int x, int y) const {
return x + y;
}
};

// 使用函数调用运算符
Functor adder;
int result = adder(3, 4); // 结果为 7

以上是关于运算符重载的一些基本概念和注意事项,具体的重载形式和规则取决于特定

的运算符和使用场景。

评论