本文是基于《C++新经典》,《Effective C++》, 《C++ primer》以及收集的相关资料总结而来.

define

#define是宏定义,一般比较常见,宏定义类似于模版,但是不如模版强大.

一般的宏为何使用do while定义

BAD case:

1
#define LOG_AND_INCREMENT(x) printf("Value: %d\n", x); x++;

然后在一个if语句中使用这个宏:

1
2
3
4
if (condition)
    LOG_AND_INCREMENT(value);
else
    // Other code

这里,由于宏只是简单的文本替换,所以上述代码实际上会被扩展为:

1
2
3
4
if (condition)
    printf("Value: %d\n", value); value++;
else
    // Other code

这会导致语法错误,因为else现在没有对应的if。实际上,value++总是会被执行,无论condition是否为true,这显然不是我们想要的。
而如果我们在宏定义中使用do while(0),就可以避免这个问题:

1
#define LOG_AND_INCREMENT(x) do { printf("Value: %d\n", x); x++; } while(0)

现在,即使在if语句中使用这个宏,也不会有问题,因为整个宏扩展后的代码都在do while(0)的作用域内:

1
2
3
4
if (condition)
    do { printf("Value: %d\n", value); value++; } while(0);
else
    // Other code

宏定义里的if

一些代码会有版本号的判断:

1
2
3
4
5
#if PI

# else

# endif

指针用法

重点关注:指向数值的指针和指针数组的区别
alt text

结构体定义

struct(结构体)是将多个不同类型或相同类型的变量组合在一起的数据结构。每个成员都有自己的内存空间,它们的内存空间是连续的,但不会重叠

1
2
3
4
5
struct Person {
    char name[50];
    int age;
    float salary;
};

共用体的定义:

union(联合体)也是将多个不同类型或相同类型的变量组合在一起的数据结构,但是它的所有成员共享同一块内存空间,也就是说,在同一时间,联合体只能存储它的一个成员的

1
2
3
4
5
union Data {
    int i;
    float f;
    char str[20];
};

typedef

可以用typedef关键字来定义新的类型名以代替已有的类型名。注意,typedef是用来定义新类型名的,不是用来定义变量的。

alt text

命名空间

命名空间定义

1
2
3
4
namespace myNamespace {
   int x;
   int y;
}
1
2
3
4
5
6

using namespace myNamespace;
// Now we can directly use x and y without namespace prefix
x = 10;
y = 20;

auto 使用

1
2
3
4
5
6
7
8
9
10
11
12
std::vector<int> vec = {12345};
// Without auto
for (std::vector<int>::iterator it = vec.begin(); it != vec.end(); ++it) {
    std::cout << *it << std::endl;
}
// With auto
for (auto it = vec.begin(); it != vec.end(); ++it) {
    std::cout << *it << std::endl;
}
// Lambda expression with auto
auto f = [](int a, int b) { return a + b; };
std::cout << f(12) << std::endl; // Outputs: 3

预防性header

预防性header

1
2
3
4
5
6
#ifndef MY_HEADER_H
#define MY_HEADER_H

// Your declarations and definitions here

#endif

注意,预处理器宏的名称通常是头文件的名称,转换为大写并用下划线替换点和其他非字母数字字符。这只是一种约定,你可以使用任何你喜欢的名称,只要它是唯一的即可。

释放数组

释放数组方式。

1
2
int x = new int[100];
delete[] x;

NULL 和 nullptr

1
2
3
4
5
6
7
8
9
10
11
12
13
void foo(int) {
std::cout << "foo(int)" << std::endl;
}

void foo(char*) {
std::cout << "foo(char*)" << std::endl;
}

int main() {
foo(NULL); // Calls foo(int), not foo(char*)
foo(nullptr); // Calls foo(char*)
return 0;
}

在这个例子中,foo(NULL)实际上调用的是foo(int),而不是foo(char*),因为NULL被视为整数0。而foo(nullptr)调用的是foo(char*),因为nullptr是一个真正的空指针。

指针使用nullptr

结构体和类的继承关系区别

C++中结构体内部成员变量及成员函数默认的访问级别是public,而C++中类的内部成员变量及成员函数的默认访问级别是private。这就是刚才的代码在定义student这个class时增加public的原因,不然外界就不能直接用“对象名.成员”的方式来访问类中的成员。
C++中结构体的继承默认是public,而C++中类的继承默认是private。后面讲解类继承时再进一步讨论

inline内联函数

模版函数、内联函数可以被放到.h文件当中

传统书写函数时一般将函数声明放在一个头文件中,将函数定义放在一个.cpp源文件中,如果要把函数定义放在头文件中,那么超过1个.cpp源文件要包含这个头文件,系统会报错,但是内联函数恰恰相反,内联函数的定义就放在头文件中,这样需要用到这个内联函数的.cpp源文件都能通过#include来包含这个内联函数的源代码,以便找到这个函数的本体(源代码)并尝试将对该函数的调用替换为函数体内的语句。

函数声明一般放在头文件中,函数定义一般放在源文件中,所以函数只能定义一次,但可以声明多次,因为多个源文件可能都包含一个头文件,而且习惯上,函数定义的源文件中也把函数

另计:为什么函数可以被多次声明,反而不可以被多次定义
在C++中,函数可以被多次声明,是因为声明只是告诉编译器函数的存在及其签名(包括函数名、返回类型和参数类型等),但并不会产生实际的代码。由于声明不会分配存储空间,因此可以在程序中的多个地方进行。

然而,函数的定义不同,它不仅声明了函数,还提供了函数的实现,即函数体。定义会在内存中分配空间,并生成实际的代码。如果函数被多次定义,编译器就会不清楚应该使用哪个定义,因此会导致错误。这就是为什么函数不能被多次定义的原因。

这个规则也被称为“一次定义规则”(One Definition Rule,ODR)。它是C++标准中的一部分,要求函数、对象和其他实体在整个程序中只能被定义一次。

函数重载

函数重载(Function Overloading)是指在同一作用域中可以定义多个同名函数,这些函数的参数列表(参数的个数或者类型)不同,或者参数的顺序不同。编译器会根据调用时提供的参数来决定使用哪个函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <iostream>

void print(int i) {
std::cout << "Here is int: " << i << std::endl;
}
void print(double f) {
std::cout << "Here is float: " << f << std::endl;
}
void print(char const *c) {
std::cout << "Here is char: " << c << std::endl;
}

int main() {
print(10);
print(10.10);
print("ten");
return 0;
}

指针和引用区别

  1. 引用更易于使用:指针需要使用特殊的语法(如星号和箭头运算符),而引用则可以像普通变量一样使用。这使得引用在语法上更简洁,更易于阅读和写作。

  2. 引用更安全:引用必须在创建时进行初始化,并且一旦初始化,就不能改变它所引用的对象。这避免了空指针和悬挂指针等常见的问题。

  3. 引用更适合某些特定的用途:例如,引用常常被用作函数的参数或返回值。这允许函数修改其参数,或返回一个非临时对象的别名。而指针则不适合这些用途,因为它们会引入额外的间接性。

然而,引用并不能完全替代指针。指针更加灵活,可以用于实现数组、链表、树等复杂的数据结构。指针还可以用于实现动态内存分配,或者创建和操作对象的动态数组。

常用基础函数

在 C++ 中,vectorunordered_mapstackqueue 是常用的 STL(标准模板库)容器。以下是这些容器的一些常用操作函数的列表。

1. vector

vector 是一个动态数组,支持随机访问。

  • 构造与初始化

    • vector<Type> v; // 创建一个空的 vector
    • vector<Type> v(size); // 创建一个指定大小的 vector
    • vector<Type> v(size, value); // 创建一个指定大小并初始化为 value 的 vector
  • 元素访问

    • v[i] // 访问第 i 个元素
    • v.at(i) // 访问第 i 个元素,带边界检查
    • v.front() // 访问第一个元素
    • v.back() // 访问最后一个元素
  • 修改元素

    • v.push_back(value) // 在末尾添加元素
    • v.pop_back() // 删除最后一个元素
    • v.insert(position, value) // 在指定位置插入元素
    • v.erase(position) // 删除指定位置的元素
    • v.clear() // 清空所有元素
  • 其他操作

    • v.size() // 返回元素个数
    • v.empty() // 检查是否为空
    • v.resize(new_size) // 调整大小
    • v.reserve(new_capacity) // 预留空间
1
2
3
4
5
6
7
8
9
10
11
12
13
#include <iostream>
#include <vector>

int main() {
std::vector<int> vec = {1, 2, 3, 4, 5};

for (const auto& value : vec) {
std::cout << value << " ";
}
std::cout << std::endl;

return 0;
}

2. unordered_map

unordered_map 是一个基于哈希表的关联容器,提供键值对存储。

  • 构造与初始化

    • unordered_map<Key, Value> um; // 创建一个空的 unordered_map
    • unordered_map<Key, Value> um{{key1, value1}, {key2, value2}}; // 初始化
  • 元素访问

    • um[key] // 访问或插入元素 使用 um[key] 访问 unordered_map 时,如果键不存在,会默认构造一个值并插入,这在某些情况下可能不是你想要的行为。因此,了解这一点并根据需要选择合适的方法是很重要的。
    • um.at(key) // 访问元素,带边界检查
    • um.find(key) // 查找元素,返回迭代器
  • 修改元素

    • um.insert({key, value}) // 插入元素
    • um.erase(key) // 删除指定键的元素
    • um.clear() // 清空所有元素
  • 其他操作

    • um.size() // 返回元素个数
    • um.empty() // 检查是否为空
    • um.count(key) // 返回指定键的元素个数

map 遍历

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <iostream>
#include <map>

int main() {
std::map<int, std::string> myMap = {
{1, "one"},
{2, "two"},
{3, "three"}
};

for (auto it = myMap.begin(); it != myMap.end(); ++it) {
std::cout << it->first << ": " << it->second << std::endl;
}
//for (const auto& pair : myMap) {
// std::cout << pair.first << ": " << pair.second << std::endl;
//}

return 0;
}

3. stack

stack 是一个后进先出(LIFO)的容器适配器。

  • 构造与初始化

    • stack<Type> s; // 创建一个空的 stack
  • 元素访问

    • s.top() // 访问栈顶元素
  • 修改元素

    • s.push(value) // 压入元素
    • s.pop() // 弹出栈顶元素
  • 其他操作

    • s.size() // 返回元素个数
    • s.empty() // 检查是否为空

4. queue

queue 是一个先进先出(FIFO)的容器适配器。

  • 构造与初始化

    • queue<Type> q; // 创建一个空的 queue
  • 元素访问

    • q.front() // 访问队头元素
    • q.back() // 访问队尾元素
  • 修改元素

    • q.push(value) // 入队元素
    • q.pop() // 出队元素
  • 其他操作

    • q.size() // 返回元素个数
    • q.empty() // 检查是否为空

static_cast、const_cast、reinterpret_cast

  • static_cast:用于安全的类型转换,编译时检查。
  • dynamic_cast:用于安全的下行转换,运行时检查,适用于多态类型。
  • reinterpret_cast:用于低级别的类型转换,不进行类型检查,可能导致未定义行为。
  • const_cast:用于添加或移除 constvolatile 属性。

1. static_cast

  • 用途:用于在相关类型之间进行转换,通常用于基本数据类型之间的转换、类层次结构中的上行和下行转换(但不安全)。
  • 特性
    • 编译时检查类型安全。
    • 可以进行隐式转换的类型之间的显式转换。
    • 不会执行运行时类型检查。
    • 一般不能用于指针转换

示例

1
2
3
4
5
class Base {};
class Derived : public Base {};

Base* b = new Derived();
Derived* d = static_cast<Derived*>(b); // 上行转换

2. dynamic_cast

  • 用途:用于安全地进行类层次结构中的下行转换(从基类到派生类)。
  • 特性
    • 仅适用于有虚函数的类(即多态类型)。
    • 在运行时进行类型检查,如果转换不合法,返回 nullptr(对于指针)或抛出 std::bad_cast 异常(对于引用)。

示例

1
2
3
4
5
6
7
class Base {
virtual void foo() {}
};
class Derived : public Base {};

Base* b = new Base();
Derived* d = dynamic_cast<Derived*>(b); // 如果 b 不是 Derived 类型,d 将为 nullptr

3. reinterpret_cast

  • 用途:用于进行低级别的类型转换,通常用于指针类型之间的转换,重新解释一块内存地址将这个内存地址转换成对应的类型。
  • 特性
    • 不进行任何类型检查,可能导致未定义行为。
    • 可以将任何指针类型转换为任何其他指针类型。
    • 适用于需要直接操作内存的场景。

示例

1
2
3
int* p = new int(42);
void* vp = reinterpret_cast<void*>(p); // 将 int* 转换为 void*
int* p2 = reinterpret_cast<int*>(vp); // 将 void* 转换回 int*

4. const_cast

  • 用途:用于添加或移除对象的 constvolatile 属性。
  • 特性
    • 只能用于指针或引用类型。
    • 允许修改原本为 const 的对象(如果原对象不是 const 的话)。

示例

1
2
3
const int x = 10;
int* p = const_cast<int*>(&x); // 移除 const 属性
*p = 20; // 未定义行为,因为原对象是 const

在使用这些类型转换时,应该根据具体的需求和上下文选择合适的转换方式,以确保代码的安全性和可维护性。