这一章在干嘛?

盖房子先搭框架。这一章带你逐行解剖两三个真实可跑的 C++ 程序,搞清楚每个程序的必备骨架:必须有 main() 函数、用 cout 输出、用 cin 输入、用变量存数据、用函数封装模块。学完你就能独立写出”输入 → 处理 → 输出”的简单程序了。

2.1 进入 C++:第一个程序

2.2 解剖 main 函数

2.3 注释、预处理器与头文件

2.4 用 cout 输出

2.5 源代码格式与空白

2.6 变量与赋值

2.7 用 cin 输入

2.8 函数初步

2.9 using 指令的位置与命名习惯

2.1 进入 C++:第一个程序

程序清单 2.1 myfirst.cpp

// myfirst.cpp -- displays a message
#include <iostream>    // a PREPROCESSOR directive
int main()             // function header
{
    using namespace std;    // make definitions visible
    cout << "Come up and C++ me some time.";    // message
    cout << endl;           // start a new line
    cout << "You won't regret it!" << endl;
    return 0;               // terminate main()
}

运行结果:

Come up and C++ me some time.
You won't regret it!

注意 C++ 区分大小写:写成 CoutCOUT 编译器会直接拒绝,说你不认识 cout。程序包含这些元素:

  • // 开头的注释(编译器无视它们);
  • 预处理器指令 #include
  • 函数头 int main()
  • using namespace 编译指令;
  • 花括号 { } 括起来的函数体;
  • 用 cout 显示消息的语句;
  • 结束 main() 的 return 语句。

如果窗口一闪而过,在 return 前加 cin.get();(必要时加两条)。另外 C++ 也能用 C 的 printf()scanf(),但本书用更聪明的 C++ 输入输出方案。

2.2 解剖 main 函数

去掉装饰后,程序的基本结构是:

int main()
{
    statements
    return 0;
}

这几行合起来叫函数定义(function definition),分两部分:第一行是函数头(function header),花括号里的部分是函数体(function body)。函数头概括了函数与程序其他部分的接口,函数体则是”让计算机干什么”的指令。C++ 中每条完整指令叫一条语句(statement),必须以分号结尾。

ch02_01

▲ 图 2.1 main() 函数

分号是终止符,不是分隔符。Pascal 用分号”分隔”两条语句,某些位置可以省略;C++(像 C 一样)把分号当作语句的组成部分——所以永远不要省略分号

函数头为什么长这样?int返回类型——main() 向调用者交回一个整数;空括号表示参数列表为空——main() 不从调用者那里接收任何信息。谁调用了 main()?是编译器加进来的启动代码,它负责在程序和操作系统之间居中协调,所以 main() 的返回值最终交给了操作系统。经典 C 风格的 main()(省略返回类型)已在 C++ 中被淘汰;int main(void)int main() 等价(C++ 中空括号就等于 void);而 void main() 不是标准的一部分,某些系统能跑、另一些会失败——别用。

标准还给了 main() 一个专属优惠:如果执行到达 main() 末尾还没遇到 return,效果等同于 return 0;。这个隐式返回只对 main() 有效,其他函数没有。

为什么必须叫 main():C++ 程序执行总是从 main() 的开头开始,没有 main() 就不是完整程序。例外情况:Windows 的动态链接库(DLL)模块、机器人控制器芯片程序等非独立程序可以没有 main()。另外拼写和大小写都要对——Main()MAIN()mane() 都不算数。

2.3 注释、预处理器与头文件

注释// 引出单行注释,可独占一行也可跟在代码后面。C 风格注释 /* ... */*/ 结尾,可以跨行,但容易忘配对,建议坚持用 //。注释是写给人看的,程序越复杂越有价值。

预处理器与 iostream#include <iostream> 让预处理器(编译前自动处理源文件的程序)把 iostream 文件的内容加进你的程序——iostream 里的 io 指 input/output,你的程序要用 cout 显示消息,就得有这个文件里的定义。使用 cin 和 cout 的程序都必须包含它。

头文件命名:像 iostream 这样的文件叫包含文件或头文件。命名惯例经过了几次演变:

头文件种类惯例示例说明
C++ 旧式以 .h 结尾iostream.h可被 C++ 程序使用
C 旧式以 .h 结尾math.hC、C++ 程序都能用
C++ 新式无扩展名iostream可被 C++ 程序使用,使用命名空间 std
转换后的 C加 c 前缀、无扩展名cmath可被 C++ 程序使用,可能使用非 C 特性如 namespace std

ANSI/ISO 委员会曾想统一给 C++ 头文件定一个 .hpp 之类的扩展名,结果谁也说服不了谁,最终”达成了一致:什么都不用”。

命名空间:不用 iostream.h 而用 iostream 时,就要配合 using namespace std;。为什么?设想两个厂商的库都有个 wanda() 函数,编译器怎么知道你要哪个?命名空间让厂商把产品装进”带标签的箱子”里:Microflop::wanda()Piscine::wanda() 各归各。标准库的东西统统装在名为 std 的箱子里,所以 cout 的全名其实是 std::cout,endl 是 std::endl

三种省事程度的写法:

using namespace std;      // 图省事:std 里所有名字都可用
 
using std::cout;          // 更克制:只开放 cout
using std::endl;          // 只开放 endl
 
std::cout << "hi";        // 最克制:每次都写全名

大型项目里 using namespace std; 被视为偷懒且有隐患,推荐后两种;简单程序用它无妨,本书开头也用它。

2.4 用 cout 输出

cout << "Come up and C++ me some time.";

双引号括起的字符系列叫字符串<< 指示信息流向——把字符串”插入”输出流。cout 是一个预定义对象:你不需要知道它的内部结构,只需要知道接口用法。这就是对象的好处——先会用,后懂原理

ch02_02

ch02_03

▲ 图 2.2 用 cout 显示字符串

运算符重载初见<< 本来是按位左移运算符,这里被赋予了”插入输出流”的新含义——同一个符号按上下文表达不同含义,就是运算符重载。C 自己也有:& 既是取地址又是按位与,* 既是乘法又是解引用。就像”sound card”和”sound financial basis”里的 sound,靠语境判断意思。

endl 操纵器cout << endl; 让屏幕光标移到下一行行首。这类对 cout 有特殊含义的记号叫操纵器(manipulator)。关键点:cout 输出完不会自动换行!省略 endl 的后果:

Come up and C++ me some time. You won't regret it!

两句话挤在了一行。想接缝处有空格,就把空格写进字符串里。

换行符 \n:更古老的方式,把 \n 嵌在字符串里更省事:

cout << "Pluto is a dwarf planet.\n";       // 显示文本并换行
cout << "Pluto is a dwarf planet." << endl; // 同样效果

区别在于:endl 保证立即刷新输出(马上显示到屏幕),"\n" 没有这个保证——某些系统上提示符可能要等你输完才显示出来。显示字符串时用 \n 省键击,其他场合用 endl。

2.5 源代码格式与空白

C++ 用分号标示语句结束,回车、空格、制表符的地位是平等的——你可以把一条语句摊成几行,也可以把几条挤成一行。下面这段”奇丑但合法”的代码完全有效:

#include <iostream>
    int
main
() { using
    namespace
    std; cout
    <<
"Come up and C++ me some time."
; cout <<
endl; cout <<
"You won't regret it!" <<
endl; return 0; }

但有红线:不能在元素中间插空白——名字中间不行,字符串中间的回车也不行(C++11 的原始字符串除外):

int ma in()              // 非法:名字里有空格
re
turn 0;                  // 非法:单词里夹回车
cout << "Behold the Beans
of Beauty!";             // 非法:字符串里夹回车

代码中不可分割的最小元素叫记号(token),记号之间一般要用空白(空格、制表符、回车)隔开;括号、逗号等单字符记号则不需要:

ch02_04

▲ 图 2.3 记号与空白

return0;      // 非法,必须写 return 0;
return(0);    // 合法,省略了空白
intmain();    // 非法,int 和 main 之间没空白
int main ( )  // 合法

惯例风格:一行一条语句;函数的左右花括号各占一行;函数体内的语句相对花括号缩进;函数名括号周围不放空白。

2.6 变量与赋值

程序清单 2.2 carrots.cpp

// carrots.cpp -- food processing program
// uses and displays a variable
 
#include <iostream>
 
int main()
{
    using namespace std;
 
    int carrots;    // declare an integer variable
 
    carrots = 25;    // assign a value to the variable
    cout << "I have ";
    cout << carrots;    // display the value of the variable
    cout << " carrots.";
    cout << endl;
    carrots = carrots - 1;    // modify the variable
    cout << "Crunch, crunch. Now I have " << carrots << " carrots." << endl;
    return 0;
}

运行结果:

I have 25 carrots.
Crunch, crunch. Now I have 24 carrots.

声明语句int carrots; 一句话干了两件事——指明需要能装一个整数的内存,并给这块内存贴上标签 carrots。为什么必须先声明变量?声明让编译器帮你抓拼写错误。BASIC 这类语言用新名字就自动创建变量,短看友好,但把 CastleDark 误打成 CastleDank,程序不报错却悄悄用错了变量,这种 bug 极难排查。C++ 里没声明的名字直接编译失败,错误当场现形。另外 C++ 不要求声明全堆在函数开头,惯例是在离首次使用最近的地方声明

ch02_05

▲ 图 2.4 变量声明

赋值语句carrots = 25; 把 25 放进 carrots 代表的内存位置。= 是赋值运算符,可以连环用,从右向左依次生效:

yamaha = baldwin = steinway = 88;  // 三个都变成 88

carrots = carrots - 1; 也展示了变量可以改值:右边的算术表达式先算(25−1=24),再存回 carrots。

cout 的新把戏cout << carrots; 打印的不是字母 c-a-r-r-o-t-s,而是变量里存的数 25。这里面有两步:把 carrots 替换成当前数值,再把数值翻译成输出字符。别忘了整数 25 和字符串 “25” 完全是两码事——后者存的是字符 ‘2’ 和 ‘5’ 的编码。对比 C 的 printf():

printf("Printing a string: %s\n", "25");
printf("Printing an integer: %d\n", 25);

printf() 靠 %s%d 这些代码指定类型,用错了它也不吭声,直接输出乱码。cout 则自动识别类型,聪明又防呆——这正是 OOP 特性带来的好处,<< 会根据后面数据的类型调整行为。

2.7 用 cin 输入

程序清单 2.3 getinfo.cpp

// getinfo.cpp -- input and output
#include <iostream>
 
int main()
{
    using namespace std;
 
    int carrots;
 
    cout << "How many carrots do you have?" << endl;
    cin >> carrots; // C++ input
    cout << "Here are two more.";
    carrots = carrots + 2;
 
    // the next line concatenates output
    cout << "Now you have " << carrots << " carrots." << endl;
    return 0;
}

运行示例:

How many carrots do you have?
12
Here are two more. Now you have 14 carrots.

cin >> carrots; 把键盘输入的字符流”提取”出来变成变量的值。cin 和 cout 是流的两端:输出是流出程序的字符流,用 << 插入;输入是流进程序的字符流,用 >> 提取——符号形状直观暗示了信息流向。cin 和 cout 一样聪明:你敲的是字符 1、2,cin 会自动把它们转成整数 12 存进 carrots。

cout 拼接<< 可以串成一长条,一行太长还能分行写:

cout << "Now you have " << carrots << " carrots." << endl;

注意”Here are two more.”和”Now you have 14 carrots.”出现在同一行——前一条 cout 输出的结尾就是后一条的起点,中间隔着别的语句也一样。

顺便认识类:cin 是 istream 类的对象,cout 是 ostream 类的对象,两个类的定义都在 iostream 文件里。类之于对象,就像类型之于变量:类是一份数据形式及其可用操作的说明书,对象是按说明书造出来的实体。用”名演员”这个类别打比方:类是”名演员”这个概念(含”读台词""表达悲伤""领奖”等操作),Kermit the Frog 则是具体的一个对象。对对象执行允许的操作 = 给对象发消息,比如用重定义的 << 向 cout 发”把这个显示出来”的消息:

ch02_06

▲ 图 2.5 向对象发送消息

2.8 函数初步

C++ 函数分两种:有返回值的和没有的。

库函数 sqrt():要算 6.25 的平方根并存进 x:

x = sqrt(6.25); // 返回值 2.5,赋给 x

sqrt(6.25)函数调用;括号里的 6.25 是传递给函数的参数(argument);函数算完送回来的 2.5 是返回值

ch02_07

▲ 图 2.6 调用函数

ch02_08

▲ 图 2.7 函数调用语法

函数原型(prototype):编译器用函数前必须先知道它的参数和返回值是什么类型。原型就是干这个的,就像变量声明之于变量:

double sqrt(double); // 函数原型:收一个 double,返回一个 double

提供原型有两种办法:自己手敲,或者包含 cmath 头文件——后者更好,因为头文件比你更不容易抄错。库中每个函数的原型都在某个头文件里,查文档确认该包含哪个。别混淆原型定义:原型只描述接口(进什么、出什么),定义才包含干活的真实代码。

程序清单 2.4 sqrt.cpp

// sqrt.cpp -- using the sqrt() function
#include <iostream>
#include <cmath>    // or math.h
 
int main()
{
    using namespace std;
 
    double area;
    cout << "Enter the floor area, in square feet, of your home: ";
    cin >> area;
    double side;
    side = sqrt(area);
    cout << "That's the equivalent of a square " << side
    << " feet to the side." << endl;
    cout << "How fascinating!" << endl;
    return 0;
}

运行示例:

Enter the floor area, in square feet, of your home: 1536
That's the equivalent of a square 39.1918 feet to the side.
How fascinating!

顺带三个知识点:声明格式统一为 类型名 变量名;;double 能存带小数的值,范围远大于 int;初始化(声明同时赋值)可以合并一步:double side = sqrt(area);。如果链接时报 _sqrt 是”未定义的外部符号”,说明编译器没有自动搜索数学库,Unix 下在命令行末尾加 -lm 选项即可。

函数的变体pow() 要两个参数(double pow(double, double);,调用如 pow(5.0, 8.0) 求 5 的 8 次方);rand() 不要参数(int rand(void););有的函数没有返回值,返回类型写 void(如 void bucks(double);),这种函数只能当独立语句调用,不能塞进表达式里。

自定义函数:程序清单 2.5 ourfunc.cpp

// ourfunc.cpp -- defining your own function
#include <iostream>
void simon(int);    // function prototype for simon()
 
int main()
{
    using namespace std;
    simon(3);    // call the simon() function
    cout << "Pick an integer: ";
    int count;
    cin >> count;
    simon(count);    // call it again
    cout << "Done!" << endl;
    return 0;
}
 
void simon(int n)    // define the simon() function
{
    using namespace std;
    cout << "Simon says touch your toes " << n << " times." << endl;
}    // void functions don't need return statements

运行示例:

Simon says touch your toes 3 times.
Pick an integer: 512
Simon says touch your toes 512 times.
Done!

函数定义的通用形式是 类型 函数名(参数列表) { 语句 }C++ 不允许函数定义嵌套——每个定义独立并列,所有函数一律平等:

ch02_09

▲ 图 2.8 函数定义在文件中依次排列

simon() 的头 void simon(int n) 表示:不返回值、收一个 int 参数。调用时 simon(3) 把 3 赋给函数体内的变量 n。因为是 void 函数,不能写 simple = simon(3);。而 main() 的头 int main() 表示要返回一个 int——返回给谁?给操作系统。很多系统会拿这个退出状态做文章:约定 0 表示程序成功,非零表示出了问题,shell 脚本就能据此决定下一步动作。

带返回值的自定义函数:程序清单 2.6 convert.cpp(1 英石 = 14 磅):

// convert.cpp -- converts stone to pounds
#include <iostream>
int stonetolb(int);    // function prototype
int main()
{
    using namespace std;
    int stone;
    cout << "Enter the weight in stone: ";
    cin >> stone;
    int pounds = stonetolb(stone);
    cout << stone << " stone = ";
    cout << pounds << " pounds." << endl;
    return 0;
}
 
int stonetolb(int sts)
{
    return 14 * sts;
}

运行示例:

Enter the weight in stone: 15
15 stone = 210 pounds.

return 14 * sts; 说明 return 后面可以跟表达式,不必非得先存进变量。嫌难读也可以走长路线:

int stonetolb(int sts)
{
    int pounds = 14 * sts;
    return pounds;
}

两版结果一样,后者把计算和返回分开,更易读易改。有返回值的函数可以出现在任何同类型常量能出现的地方:

int aunt = stonetolb(20);
int aunts = aunt + stonetolb(10);
cout << "Ferdie weighs " << stonetolb(16) << " pounds." << endl;

函数原型描绘的是”黑盒”视角:参数列表是流入,函数类型是流出:

ch02_10

▲ 图 2.9 函数原型与作为黑盒的函数

stonetolb() 虽短,却五脏俱全:有头有体、收参数、返值、要原型——把它当作函数设计的标准模板。

关键词:int、void、return、double 是 C++ 的关键词(keyword),语言专用,不能拿来当变量或函数名,但可以作名字的一部分(如 painter 里藏着 int)。注意 main 不是关键词——它是必需函数的名字。把 cout 同时用作对象名和变量名会迷惑编译器,同一个函数里别这么干。

2.9 using 指令的位置与命名习惯

多函数程序里,using namespace std; 放哪里?清单 2.5 在两个函数里各放了一条——每个用到 cout 的函数都需要访问 std。也可以放在所有函数定义的外面、上面,让整个文件的所有函数共享。主流理念是按需开放:只有 main() 用 cout 就只放 main() 里。汇总一下你的选择:

  • 文件级 using namespace std;——std 的所有名字对文件内每个函数开放;
  • 函数级 using namespace std;——只对该函数开放;
  • 函数级 using std::cout;——只开放指定名字;
  • 全不写,用时带前缀:std::cout << "..." << std::endl;

命名习惯:函数、类、变量怎么起名,纯看团队口味——myFunction()my_function()myfunct() 都合法。一致、精确的个人命名风格是良好软件工程的标志,值得从现在就养成。

常见坑:分号与大小写

C++ 的分号是语句的一部分,漏写必报错;而 cout 写成 Cout、COUT 也必报错——C++ 严格区分大小写和拼写。新手期一半以上的编译错误来自这两处,看到 unknown identifier(未知标识符)先检查拼写和大小写。

常见坑:void main() 和忘写 endl

void main() 在某些系统上能编译通过,但它不在 C++ 标准里,换台机器就挂——请写 int main()。另一个坑:cout 输出不自动换行,忘加 endl(或 \n)时输出会全部挤在同一行,且两条输出之间哪怕隔着其他语句也照样接着上一条尾巴写。

通关标准:

你能默写一个完整的最小程序(#include、int main()、return 0)并解释每一行的作用;能说出 cout <<cin >> 各自的信息流向;能自定义一个带参数、带返回值的函数并在 main() 里调用它(原型 + 定义一个不少)。