「C++ 基础」基础语法
由于许多语法与C十分相似,这里只记录一些对C的扩充点以及之前学习的疏漏。
注释
除了常用的//
,/*...*/
,还可以用条件编译来写,这样可以实现嵌套:
1 |
|
测试时可以使用 #if 1
来执行测试代码,发布后使用 #if 0
来屏蔽测试代码。
数据类型
枚举类型(enumeration
)是C++中的一种派生数据类型,它是由用户定义的若干枚举常量的集合。
如果一个变量只有几种可能的值,可以定义为枚举(enumeration)类型,变量的值只能在列举出来的值的范围内。
创建枚举,需要使用关键字 enum
。枚举类型的一般形式为:
1 | enum 枚举名{ |
每个枚举元素在声明时被分配一个整型值,默认从 0 开始,逐个加 1。也可以在定义枚举类型时对枚举元素赋值,此时,赋值的枚举值为所赋的值,而其他没有赋值的枚举值在为前一个枚举值加 1。如下例,green 的值为 5, blue 的值为 6。
1 | enum color { red, green=5, blue }; |
变量
声明和定义的区别:
定义包含了声明,但是声明不包含定义。声明是不会为变量开辟内存空间的,因此无法进行初始化
1 | int a = 0; //定义并声明了变量 a |
函数类似,如果只是声明,编译器只知道有这么个函数,具体函数怎么定义的要编译器去找。C/C++ 编译 cpp
文件是从上往下编译,所以 main 函数里面调用其他函数时,如果其他函数在 main 函数的下面,则要在 main 函数上面先声明这个函数。
1 | void fun1(); //函数声明 |
存储类
存储类定义 C++ 程序中变量/函数的范围(可见性)和生命周期。
register
register 存储类用于定义存储在寄存器中而不是 RAM 中的局部变量。因此变量的最大尺寸等于寄存器的大小(通常是一个词),且不能对它应用一元的 ‘&’ 运算符(因为它没有内存位置)。
1 | for(register int i = 0; i ...) // 算法比赛中为了加速经常看到 |
static
static 存储类指示编译器在程序的生命周期内保持局部变量的存在,而不需要在每次它进入和离开作用域时进行创建和销毁。因此,使用 static 修饰局部变量可以在函数调用之间保持局部变量的值。
static 修饰符也可以应用于全局变量。当 static 修饰全局变量时,会使变量的作用域限制在声明它的文件内。
以及我们已知的:当 static 用在类数据成员上时,会导致仅有一个该成员的副本被类的所有对象共享。
extern
常用于用于不同文件之间的变量和函数的传递。
main.cpp
:
1 |
|
support.cpp
:
1 |
|
函数
函数参数
当调用函数时,有三种向函数传递参数的方式:
- 传值调用(相比指针与引用调用其实没什么优点)
- 指针调用
- 引用调用
默认情况下,C++ 使用传值调用来传递参数。一般来说,这意味着函数内的代码不能改变用于调用函数的参数。
Lambda 函数
1 | [capture](parameters) mutable ->return-type{body} |
[capture]
:捕捉列表。[]
其实是 lambda 引出符。编译器根据该引出符判断接下来的代码是否是 lambda 函数。捕捉列表能够捕捉上下文中的变量供 lambda 函数使用。
1 | [] // 沒有定义任何变量。使用未定义变量会引发错误。 |
-
(parameters)
:参数列表。可选。 -
mutable
:默认情况下,lambda 函数总是一个 const 函数,mutable 可以取消其常量性。在使用该修饰符时,参数列表不可省略(即使参数为空)。 -
->return_type
:不需要返回值的时候也可以连同符号->
一起省略。此外,在返回类型明确的情况下,也可以省略该部分,让编译器对返回类型进行推导。 -
{statement}
:函数体。内容与普通函数一样,不过除了可以使用参数之外,还可以使用所有捕获的变量。
例:
1 | [](int x, int y) -> int { int z = x + y; return z + x; } |
字符串
除c风格字符串外,C++ 标准库也提供了 string
类
引用
引用变量是一个别名,也就是说,它是某个已存在变量的另一个名字。一旦把引用初始化为某个变量,就可以使用该引用名称或变量名称来指向变量。
与指针的区别
- 不存在空引用。引用必须连接到一块合法的内存。
- 一旦引用被初始化为一个对象,就不能被指向到另一个对象。指针可以在任何时候指向到另一个对象。
- 引用必须在创建时被初始化。指针可以在任何时间被初始化。
1 | double d; |
引用作为参数
1 | // 加上 const 关键字的目的,是不希望 func 修改原始数据 |
与传递实参相比:
- 从内存使用的角度来说,传递实参,则会将数据拷贝过去(创建了副本)
- 既然要创建副本,就意味着效率更低。
与传指针相比:
- 选择成员的时候,引用使用点
.
来查找,而指针则使用->
来查找。 - 指针可能传递一个
NULL
过来,因此在使用前必须检查有效性;引用则必然代表某个对象,不需要做此检查。
引用作为返回值
1 | double& setValues(int i) { |
要注意被引用的对象不能超出作用域。所以返回一个对局部变量的引用是不合法的,但是,可以返回一个对静态变量的引用。
1 | int& func() { |
基本 I/O
<iostream>
中定义了 cin
、cout
、cerr
和 clog
对象,分别对应于标准输入流、标准输出流、非缓冲标准错误流和缓冲标准错误流。
流是字节序列。如果字节流是从设备(如键盘、磁盘驱动器、网络连接等)流向内存,就叫做输入操作。如果字节流是从内存流向设备(如显示屏、打印机、磁盘驱动器、网络连接等),就叫做输出操作。
输入输出常用函数:
1 | cout<<setiosflags(ios::left|ios::showpoint); // 设左对齐,以一般实数方式显示 |
预定义的对象 cerr
是 iostream
类的一个实例。附属到标准错误设备,通常也是显示屏,但是 cerr 对象是非缓冲的,且每个流插入到 cerr 都会立即输出。
clog 对象附属到标准错误设备,通常也是显示屏,但是 clog 对象是缓冲的。这意味着每个流插入到 clog 都会先存储在缓冲区,直到缓冲填满或者缓冲区刷新时才会输出。
1 |
|
一般使用 cerr 流来显示错误消息,而其他的日志消息则使用 clog 流来输出。
文件读写
在<fstream>
库中:
ofstream
数据类型,表示输出文件流,用于创建文件并向文件写入信息。ifstream
数据类型,表示输入文件流,用于从文件读取信息。fstream
数据类型,表示文件流,且同时具有 ofstream 和 ifstream 两种功能
打开文件:
1 | void open(const char *filename, ios::openmode mode); |
被打开的模式有:
模式标志 | 描述 |
---|---|
ios::app | 追加模式。所有写入都追加到文件末尾。 |
ios::ate | 文件打开后定位到文件末尾。 |
ios::in | 打开文件用于读取。 |
ios::out | 打开文件用于写入。 |
ios::trunc | 如果该文件已经存在,其内容将在打开文件之前被截断,即把文件长度设为 0。 |
1 | ofstream outfile; |
关闭文件:
当 C++ 程序终止时,它会自动关闭刷新所有流,释放所有分配的内存,并关闭所有打开的文件。
但在程序终止前关闭打开文件是一个好习惯。
1 | void close(); |
读写:
1 | outfile << data << endl; |
文件位置指针:
关于 istream 的 seekg(”seek get”)和关于 ostream 的 seekp(”seek put”)。
seekg 和 seekp 的参数通常是一个长整型。第二个参数可以用于指定查找方向。查找方向可以是 ios::beg(默认的,从流的开头开始定位),也可以是 ios::cur(从流的当前位置开始定位),也可以是 ios::end(从流的末尾开始定位)。
文件位置指针是一个整数值,指定了从文件的起始位置到指针所在位置的字节数。
1 | // 定位到 fileObject 的第 n 个字节(假设是 ios::beg) |
结构体
根据原先 C98 的标准,结构体定义的时候需要使用 typedef。但是对于更新的 C99 标准和 C11 及以上的标准,typedef 可以省略或者强制省略。
1 | //C98 |
现在的新标准(通用的)会变成:
1 | struct edge{ |
对于结构体类型的变量,我们可以限制成员的位数大小。
1 | struct demo{ |
这样就限制了 demoint 成员只占一个 B。
内存对齐
为什么要内存对齐?
- 为了提高内存的访问效率,比如 intel 32位 cpu,每个总线周期都是从偶地址开始读取32位的内存数据,如果数据存放地址不是从偶数开始,则可能出现需要两个总线周期才能读取到想要的数据,因此需要在内存中存放数据时进行对齐。
- 核心思想是空间换时间。
对齐设计到这3个规则:
- 对于结构体的各个成员,第一个成员的偏移量是0,排列在后面的成员其当前偏移量必须是当前成员类型的整数倍
- 结构体内所有数据成员各自内存对齐后,结构体本身还要进行一次内存对齐,保证整个结构体占用内存大小是结构体内最大数据成员的最小整数倍
- 如程序中有
#pragma pack(n)
预编译指令,则所有成员对齐以n字节为准(即偏移量是n的整数倍),不再考虑当前类型以及最大结构体内类型
例如:
1 | struct A{ |
第一个成员a是char类型,占用1个字节空间,偏移量为0,第二个成员b是int类型,占用4个字节空间,按照规则1,b的偏移量必须是int类型的整数倍,所以编译器会在a变量后面插入3字节缓冲区,保证此时b的偏移量(4字节)是b类型的整数倍(当前恰好是1倍),第3个成员c为short类型,此时c的偏移量正好是4+4=8个字节,已经是short类型的整数倍,故b与c之间不用填充缓冲字节。但这时,结构体A的大小为8+2=10个字节,按照规则2,结构体A大小必须是其最大成员类型int的整数倍,所以在10个字节的基础上再填充2个字节,保证最后结构体大小为12
1 | struct BD { |
运行结果是 sizeof(BD) = 2 + 2 + 13 +3 = 20
,这是因为计算union成员的偏移量时,需要根据union内部最大成员类型来进行缓冲补齐,所以为了保证偏移量为union最大成员int类型的整数倍,需要在number(short类型)后面填充2个字节。
一些细碎的问题
int main()
和int main(void)
区别
C 语言中,int main(void)
指的是此函数的参数为空,不能传入参数,而int main()
代表编译器对是否接受参数保持沉默,此时可以传入参数。
在 C++ 中 int main()
和 int main(void)
是等效的。
.h
和.cpp
的联系与区别
项目的增大促生了这一规范:分出了头(.h)
文件和实现(.cpp)
文件,并且也有了Package
的概念。
简单而言,.h
文件用于定义类型,便于其他文件使用和模块化。.cpp
用于实现。
例:
1 | //point.h |
1 | //point.cpp |
头文件规范
头文件的所有内容,都必须包含在
1 |
|
这样才能保证头文件被多个其他文件引用(include)时,内部的数据不会被多次定义而造成错误
非模板类型
全局类型
申明写在.h文件。对于函数来讲,没有实现体的函数,就相当于是申明;而对于数据类型(包括基本类型和自定义类型)来说,其申明就需要用extern来修饰。
然后在.cpp文件里定义、实现或初始化这些全局函数和全局变量。
自定义类型
对于自定义类型,包括类(class)和结构体(struct),它们的定义都是放在.h文件中。其成员的申明和定义较为复杂,可以参考这篇博客
模板类型
在定义模板的时候,编译器并不会对它进行编译,因为它没有一个实体可用。只有模板被具体化(specialization)之后(用在特定的类型上),编译器才会根据具体的类型对模板进行编译。
因为模板的这种特殊性,它并没有自己的准确定义,因此我们不能把它放在.cpp文件中,而要把他们全部放在.h文件中进行书写。这也是为了在模板具体化的时候,能够让编译器可以找到模板的所有定义在哪里,以便真正的定义方法。
typedef
与 #define
的区别
执行时间不同
关键字typedef
在编译阶段有效,由于是在编译阶段,因此typedef
有类型检查的功能。#define
则是宏定义,发生在预处理阶段,也就是编译之前,只进行简单的字符串替换,而不进行任何检查。功能有差异
typedef
用来定义类型的别名,定义与平台无关的数据类型,与struct
的结合使用等。#define
不只是可以为类型取别名,还可以定义常量、变量、编译开关等。作用域不同
#define
没有作用域的限制,只要是之前预定义过的宏,在以后的程序中都可以使用。
而typedef
有自己的作用域。