这篇笔记我们介绍C++语言中的基础语法,我们将从一个简单的Hello World程序开始,逐步介绍代码的每个部分。
前一章节我们已经编写了最基础的C++程序,这里我们再逐行回顾一下。
#include <iostream>
int main() {
std::cout << "Hello, world!" << std::endl;
return 0;
}
代码中,第一行使用了#include
指令引入iostream
,学习C语言时我们已经了解过#include
指令用于引入头文件,iostream
是C++标准库中的一个头文件,其中包含了基本输入输出相关的函数。main
是我们的入口函数,这个入口函数和C语言类似,它返回一个整型值代表程序的运行结果,返回0
表示程序正常运行结束,不过和C语言不同的是,C++中不推荐在无参数函数的括号内标注void
(尽管这样写也不报错)。
std::cout
是C++标准库中提供的输出内容到标准输出(默认是控制台)的对象,确切的说std::cout
是一个std::ostream
类型的对象,std
是标准库中使用的命名空间,C++中作用域解析运算符::
用来指定标识符(变量、函数、类等)属于哪个命名空间、类、或者全局作用域。此外,这里的<<
并不是左移运算符,它是一个被重载的运算符,用于向输出流对象中写入数据。运算符重载是C++中的一个高级特性,我们将在后续章节学习。至于std::endl
,它封装了一个输出操作,用于输出一个换行符并刷新输出流的缓冲区,通常配合std::cout
使用。
代码的最后,我们使用了return
语句,主函数执行结束并返回了整数0
,意味着程序正常结束。
标识符是用于命名变量、函数、类等程序实体的字符序列,我们编写代码时,标识符不是随意起名的,它通常需要遵循一定的规则:
a
、b
、c
等命名下面例子代码中,我们定义了3个变量,它们在C++代码中就是一种标识符。
int num1 = 1;
int num2 = 2;
int sum = num1 + num2;
C++保留了一些特殊关键字,我们命名标识符时不可以使用这些关键字。
asm do if return typedef
auto double inline short typeid
bool dynamic_cast int signed typename
break else long sizeof union
case enum mutable static unsigned
catch explicit namespace static_cast using
char export new struct virtual
class extern operator switch void
const false private template volatile
const_cast float protected this wchar_t
continue for public throw while
default friend register true
delete goto reinterpret_cast try
作用域是指变量或函数在程序中可见和可访问的范围。
局部作用域:在函数或代码块(即大括号{}
)内声明的变量具有局部作用域,局部作用域的变量就是我们平时说的局部变量。下面代码中,变量a
具有函数foo()
内的局部作用域。
void foo() {
int a = 1;
}
块作用域:块作用域指的是变量在条件或循环语句包围的代码块(即大括号{}
)内有效,下面例子中i
具有块作用域,在循环语句外i
不能被访问。
for (int i = 1; i <= 10; i++) {
std::cout << i << std::endl;
}
全局作用域:在所有函数外部声明的变量具有全局作用域,只要是在定义全局作用域变量的翻译单元中或者通过extern
声明引入了它,在任何位置就都可访问全局变量。
int a = 1;
void foo() {
}
命名空间作用域:命名空间(namespace)是C++中用于防止标识符冲突的一种机制,这在大型项目或使用多个库时非常有用,C++标准库就使用了std
命名空间,我们也可以定义自己的命名空间。下面例子代码中,我们定义了自己的命名空间demo
,这样引命名空间内部的函数print()
时就需要使用demo::print
,这样如果在其它命名空间中也有名为print()
的函数,由于位于不同的命名空间,它们也不会产生冲突(大型纯C语言项目中标识符冲突是个很常见且让人恼火的问题)。
#include <iostream>
namespace demo {
void print() {
std::cout << "Hello, world!" << std::endl;
}
}
int main() {
demo::print();
return 0;
}
如果你多次使用demo::print
觉得很烦,可以使用using
语句简化这一写法,下面是一个例子。
#include <iostream>
namespace demo {
void print() {
std::cout << "Hello, world!" << std::endl;
}
}
using demo::print;
int main() {
print();
return 0;
}
代码中,我们提前声明了demo::print
,这意味着我们告诉编译器print
这个标识符是demo
命名空间下的,我们后续就可以省略的写作print()
即可,不必再带着demo::
了。
对于命名空间还有一种写法是using namespace
,它告知编译器后面代码默认使用某个命名空间,使得我们可以省略该命名空间下的全部::
操作符。例如using namespace std
意味着我们默认使用std
命名空间,这样使用cout
、endl
等时全都可以省略std::
了。
#include <iostream>
using namespace std;
int main() {
cout << "Hello, world!" << endl;
return 0;
}
大部分人都对using namespace
的使用持反对意见,在大型项目中乱用using namespace
这种写法很容易造成不易察觉的标识符冲突,个人建议using namespace
的使用仅限于using namespace std
,或者彻底忘记它,完全不要用这种写法。
C++有单行注释和多行注释两种注释形式。
#include <iostream>
int main() {
// 单行注释
/*多行注释
可以跨域多行*/
std::cout << "Hello, world!" << std::endl;
return 0;
}
注意:多行注释不可以嵌套。
编译预处理用于在正式编译前执行的文本处理操作。C++中我们经常能碰到两种编译预处理指令,一种是引入头文件使用的#include
。
#include <iostream>
另一种是条件编译指令,包括#ifdef
、#ifndef
、#endif
。条件编译指令有很多用途,例如根据编译参数决定是否开启调试代码、定义头文件时用于避免头文件重复包含等。
#ifndef STACK_H
#define STACK_H
#endif
宏定义本质上是一种发生在编译阶段的文本替换,简单的宏定义可以用于定义“常量”,下面是一个例子。
#include <iostream>
#define PI 3.14159265
int main() {
std::cout << PI << std::endl;
return 0;
}
不过由于C++本身已经有常量相关的语法,宏定义常量就相对来说比较鸡肋了,宏定义没有类型检查和作用域支持,应谨慎使用。此外宏定义还可以带参数形成宏函数,但C/C++的宏编程非常难而且复杂的宏定义可读性极低,复杂的宏编程是有点类似“炫技”的高级技巧,它很容易导致项目代码可读性急剧下降并趋于不可维护,一般场景应该避免,具体可以参考C语言中相关章节。