C++ PART1 基础语法
本文是基于《C++新经典》,《Effective C++》, 《C++ primer》以及收集的相关资料总结而来.
define
#define是宏定义,一般比较常见,宏定义类似于模版,但是不如模版强大.
一般的宏为何使用do while定义
BAD case:
1 |
然后在一个if语句中使用这个宏:
1 | if (condition) |
这里,由于宏只是简单的文本替换,所以上述代码实际上会被扩展为:
1 | if (condition) |
这会导致语法错误,因为else现在没有对应的if。实际上,value++总是会被执行,无论condition是否为true,这显然不是我们想要的。
而如果我们在宏定义中使用do while(0),就可以避免这个问题:
1 |
现在,即使在if语句中使用这个宏,也不会有问题,因为整个宏扩展后的代码都在do while(0)的作用域内:
1 | if (condition) |
宏定义里的if
一些代码会有版本号的判断:
1 |
指针用法
重点关注:指向数值的指针和指针数组的区别
结构体定义
struct
(结构体)是将多个不同类型或相同类型的变量组合在一起的数据结构。每个成员都有自己的内存空间,它们的内存空间是连续的,但不会重叠
1 | struct Person { |
共用体的定义:
union
(联合体)也是将多个不同类型或相同类型的变量组合在一起的数据结构,但是它的所有成员共享同一块内存空间,也就是说,在同一时间,联合体只能存储它的一个成员的
1 | union Data { |
typedef
可以用typedef关键字来定义新的类型名以代替已有的类型名。注意,typedef是用来定义新类型名的,不是用来定义变量的。
命名空间
命名空间定义
1 | namespace myNamespace { |
1 |
|
auto 使用
1 | std::vector<int> vec = {1, 2, 3, 4, 5}; |
预防性header
预防性header
1 |
|
注意,预处理器宏的名称通常是头文件的名称,转换为大写并用下划线替换点和其他非字母数字字符。这只是一种约定,你可以使用任何你喜欢的名称,只要它是唯一的即可。
释放数组
释放数组方式。
1 | int x = new int[100]; |
NULL 和 nullptr
1 | void foo(int) { |
在这个例子中,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 |
|
指针和引用区别
-
引用更易于使用:指针需要使用特殊的语法(如星号和箭头运算符),而引用则可以像普通变量一样使用。这使得引用在语法上更简洁,更易于阅读和写作。
-
引用更安全:引用必须在创建时进行初始化,并且一旦初始化,就不能改变它所引用的对象。这避免了空指针和悬挂指针等常见的问题。
-
引用更适合某些特定的用途:例如,引用常常被用作函数的参数或返回值。这允许函数修改其参数,或返回一个非临时对象的别名。而指针则不适合这些用途,因为它们会引入额外的间接性。
然而,引用并不能完全替代指针。指针更加灵活,可以用于实现数组、链表、树等复杂的数据结构。指针还可以用于实现动态内存分配,或者创建和操作对象的动态数组。
常用基础函数
在 C++ 中,vector
、unordered_map
、stack
和 queue
是常用的 STL(标准模板库)容器。以下是这些容器的一些常用操作函数的列表。
1. vector
vector
是一个动态数组,支持随机访问。
-
构造与初始化
vector<Type> v;
// 创建一个空的 vectorvector<Type> v(size);
// 创建一个指定大小的 vectorvector<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. unordered_map
unordered_map
是一个基于哈希表的关联容器,提供键值对存储。
-
构造与初始化
unordered_map<Key, Value> um;
// 创建一个空的 unordered_mapunordered_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 |
|
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
:用于添加或移除const
或volatile
属性。
1. static_cast
- 用途:用于在相关类型之间进行转换,通常用于基本数据类型之间的转换、类层次结构中的上行和下行转换(但不安全)。
- 特性:
- 编译时检查类型安全。
- 可以进行隐式转换的类型之间的显式转换。
- 不会执行运行时类型检查。
- 一般不能用于指针转换
示例:
1 | class Base {}; |
2. dynamic_cast
- 用途:用于安全地进行类层次结构中的下行转换(从基类到派生类)。
- 特性:
- 仅适用于有虚函数的类(即多态类型)。
- 在运行时进行类型检查,如果转换不合法,返回
nullptr
(对于指针)或抛出std::bad_cast
异常(对于引用)。
示例:
1 | class Base { |
3. reinterpret_cast
- 用途:用于进行低级别的类型转换,通常用于指针类型之间的转换,重新解释一块内存地址将这个内存地址转换成对应的类型。
- 特性:
- 不进行任何类型检查,可能导致未定义行为。
- 可以将任何指针类型转换为任何其他指针类型。
- 适用于需要直接操作内存的场景。
示例:
1 | int* p = new int(42); |
4. const_cast
- 用途:用于添加或移除对象的
const
或volatile
属性。 - 特性:
- 只能用于指针或引用类型。
- 允许修改原本为
const
的对象(如果原对象不是const
的话)。
示例:
1 | const int x = 10; |
在使用这些类型转换时,应该根据具体的需求和上下文选择合适的转换方式,以确保代码的安全性和可维护性。