C++的基本语法概念整理。
C++基础快速入门
C++的大部分基本要素:内置类型
、库类型
、类类型
、变量
、表达式
、语句
、函数
。
Main函数
main函数
:每个C++程序必须含有main函数
,main函数有且只有一个,并且main函数是(唯一)被操作系统显式调用的函数。main函数的返回值
:
1.main函数通过返回值来确定程序是否成功执行完毕。返回值0则表明程序成功执行完毕。
2.main函数的返回值是一个状态指示器
。任何其他非零返回值
都有操作系统定义的含义。通常非零返回值表明有错误出现。
2.main函数的返回值必须是int型,该类型表示整数。
main函数原型
:
int main(
void
)
int main(int argc
,char** agrv
)
源文件命名规范
程序文
件称作源文件。文件后缀表示该文件是程序。文件后缀通常也表明程序是用什么语言编写的,以及选择哪一种编译器运行。
初窥输入/输出
C++并没有直接定义进行输入或输出(IO)的任何语句,这种功能是由标准库提供的。
处理格式化输入好输出的iostream库。iostream的基础是两种命名为istream和ostream的类型。iostream由istream和ostream派生而来
,在此只做基本了解,标准IO库的详细内容将在第八章详解。
标准输入与输出对象
标准库定义了4个IO对象。
缓冲标准输入(cin)
:处理输入时使用命名为cin
(读作see-in)的istream
类型对象。
cin
为缓冲流。
从键盘输入的数据保存在缓冲区中,当要提取时,是从缓冲区中取数据。
如果一次输入过多,会留在那儿慢慢用,如果输入错了,必须在回车之前修改,如果回车键按下就无法挽回了(cin会刷新缓冲区)。只有把输入缓冲区中的数据取完后,才要求输入新的数据。不可能用刷新来清除缓冲区,所以不能输错,也不能过多输入数据!
缓冲标准输出(cout)
:处理输出时使用命名为cout
(读作see-out)的ostream
类型对象。
cout
是在终端显示器输出,cout
流在内存中对应开辟了一个缓冲区
,用来存放流中的数据,当向cout
流插入一个endl
,不论缓冲区是否满了(刷新缓冲区
),都立即输出流中所有数据,然后插入一个换行符
。
无缓冲标准错误(cerr)
:通常来输出警告和错误信息给程序的使用者(读作see-err)。没有缓冲
,发送给它的内容立即被输出缓冲标准错误(clog)
:用于产生程序执行的一般信息(读作see-log)。
clog和cerr的区别
:区别在于cerr不经过缓冲区,直接向显示器输出信息,而clog中的信息存放在缓冲区,缓冲区满或者遇到endl时才输出.
C++中cin
、cout
,cerr
和C的stdin
、stdout
、stderr
都是同步的,即iostream对象和C语言stdio流是同步的,同步关系如下:
同步即表明我们可以在程序中混合用cout和printf或其他对应的流对。
可以用std::ios_base::sync_with_stdio(false)来取消这种同步,取消后,如下程序中cout和printf就不是按照预期的顺序输出:
1 | std::ios_base::sync_with_stdio(false); //关闭流同步 |
运行结果:
2 2 2 2 2 2 2 2 2 2
删除std::ios_base::sync_with_stdio(false);
这条代码或者改为true
后(开启流同步),运行结果如下:
1 2 1 2 1 2 1 2 1 2 1 2 1 2 1 2 1 2 1 2
正是因为这种同步,所以cin、cout
比scanf、printf
速度要慢,如果我们在使用cin
、cout
输入输出前加一句std::ios_base::sync_with_stdio(false)
,其实速度和scanf
、printf
差不多。
参考文章:
什么是刷新缓冲区?
以缓冲方式打开一个文件时,往文件里写几个字节,一般不会立即真正把这几个字节写入文件,只有当缓冲区满时才真正写盘。如果想在缓冲区满之前写盘保存,可以做刷新缓冲区
动作(endl)。
下列行为引发刷新缓冲区动作:
1.缓冲区满时;
2.行输出时遇endl,cerr或cin时;
3.执行冲刷函数;
4.关闭文件
输入输出操作符
输出操作符(<<
操作符) AND 输入操作符(>>
操作符)endl
:只能在输出时使用(cout<<"HelloWorld"<<endl
而不能使用(cin>>ival>>endl
))。endl是一个特殊值,称为操纵符(manipulator),将它写入流时,具有换行
的效果,并刷新与设备相关联的缓冲区
。通过刷新缓冲区,用户可以立即看到写入到流中的输出。
注意
:在调式过程中插入的输出语句都应该刷新输出流。忘记刷新输出流可能会造成输出停留在缓冲区中,如果程序崩溃,将会导致对程序崩溃位置的错误判断
表达式
表达式
是由运算符
和操作数
组合构成的。操作数
是运算符
操作的对象.注意
:表达式无分号,表达式后面加分号就成了语句。操作数
可以是常量
、变量
或者二者的组合
。
一些表达式多是多个较小的表达式和组合,这些小的表达式称为子表达式
。
每一个表达式
都有一个值
,系统按照运算符优先级
描述的顺序来完成运算。
关于注释
*
注释
:注释可以帮助其他人悦读程序,通常概括算法、确认变量的用途或者阐明难以理解的代码段。注释并不会增加可执行程序的大小,编译器会忽略所有注释。*
C++中有单行注释
和成对注释
两种.
1.单行注释:以双斜线(//)开头,一行中处于双斜线右边的内容是注释。
2.成对注释:注释对是从C语言集成过来的。成对注释以”/*
开头,以”*/
“结尾。比阿尼器把落入注释对”/**/
“之间的内容作为注释。
任何允许有制表符、空格或换行符的地方都允许放注释对。注释对可以跨越多行。
1.注释对一般用于多行解释
2.双斜线注释则常用语半行或者单行的标记(行的开头放//)
3.错误的注释比没有注释更糟
注意
:注释对不可嵌套。注释对总是以/*
开始并以*/
结束。一个注释对不能出现在另一对注释对中。
控制结构
while语句
while
提供了迭代执行的功能(循环执行).while结构形式
:while(condition
)while_body_statement
;while
通过测试condition(条件)
执行相关的while_body_statement
来重复执行
,直到condition为假(false
)。
条件是一个可求值的表达式,所以可以测试其结果。
如果结果值非零,那么条件为真(true
)。如果值为零,则条件为假(false
)。条件必须具有终止条件(循环过程中条件值也应逐步接近终止值(false)),否则就进入死循环
for循环
for循环结构形式:
for(initialize
;Condition
;count
)for_body_statement
;initialize(初始化)
和count(迭代器)
可以省略,但是在函数体中必须要有
能使Condition趋向于终止条件的表达式。
注意
:在varName或者在函数体内创建的变量,在循环结束后这些变量均不可再访问。因为在循环结束时这些变量就已经被销毁了。
if语句
if发一句结构形式:
if(
condition
){true_body_statement;
}else{else_body_statement;
}
如果condition
为真,则执行true_body_statement;
如果condition
为假,则执行else_body_statement;
**注意
**:在if语句中,true_body_statement
语句和else_body_statement
语句永远不会同时执行(只能执行一个)。
读入未知数目的输入
1 |
|
该循环中使用istream对象
作为条件,结果是测试流的状态。如果流是有效的(读入下一个输入时可能的),那么测试成功。
遇到*文件结束符
*(end-of-file)或遇到无效输入
时(如读取了一个不是整数的值(因为value为整型)),则istream对象
时无效的。处于无效状态的istream对象将导致条件失败。
在遇到文件结束符(或者一些其他的输入错误)之前,测试会成功并且执行while循环。
从键盘输入文件结束符
Windows
:Ctrl + ZUnix
:Ctrl + D
一旦测试失败,则执行while之后的语句。std::cout<<"Sum is:"<<sum<<std::endl;
该语句在输出sum后输出endl,endl输出换行并刷新与cout相关联的缓冲区。
最后执行return 0;
,通常表示程序成功运行完毕。
类的简介
C++中我们可以通过定义**
类
来定义自己的数据结构
。类机制
是C++中最重要的特性之一。
C++设计的主要焦点就是是所定义的类类型
**(class type)的行为可以向内置类型一样自然。
一般来说,类类型
存储在一个文件中,通常文件名和定义在头文件中的类名是一样的。类类型
**可以像内置类型
一样,可以定义
**类类型的变量。
例:sales_item item;
表示item是类型sales_item的一个对象。
可以简称为”一个sales_item对象”或者”一个sales_item”
除了可以定义**类类型
的变量,还可以执行类对象
**的以下操作:
1.使用
加法操作符
,+
,将两个类对象相加
2.使用输入操作符
,>>
,来读取一个类对象
3.使用输出操作符
,<<
,来输出一个类对象
4.使用赋值操作符
,=
,将一个类对象赋值给另一个类对象
**注意
**:涉及到两个以上的类对象的操作
则这两个类对象
应该是同一个类类型
。
程序中包含自定义头文件时,应采用双引号(“”)把头文件包括起来。
例:#include "sales_item.h"
**注意
**:标准库的头文件用尖括号<>括起来非标准库的头文件用双引号””括起来。
初窥成员函数
什么是成员函数?
成员函数是由类定义的函数
,有时称为类方法
(method)。
成员函数只定义一次,但被视为每个对象的成员。
之所以将这些操作成为成员函数
是因为它们(通常)在特定的对象上操作。即它们是对象的成员
,即使同一类型所有对象共享同一个定义也是如此。
当调用成员函数时,通常指定函数要操作的对象。语法是使用点操作符(
.
):
例:item.sales_isbn
点操作符
通过它的左操作数
取得右操作数
。点操作符
仅应用于类类型的对象
:左操作数
必须是类类型的对象
,右操作数
必须指定该类型的成员
。点操作符(.)
的右操作数
不是对象或值,而是成员的名字
。执行成员函数
和执行其他函数相似:要调用函数,可将调用操作符(())放在函数名之后
。调用操作符
是一对圆括号,括住
传递给函数的实参列表
(可能为空)。)。
C++的变量和基本类型
程序设计语言拥有一些共同的特征
:
内置数据类型
,如整型、字符型等。表达式语句
:表达式语句用于操纵上述类型的值。变量
:程序员可以使用变量所作用的对象命名。控制结构
:如if或while,程序员可以使用控制结构有条件地执行或重复执行一组动作。函数
:程序员可以使用函数把行为抽象成可调用的计算单元。C++是
静态类型(statically typed)
语言,在编译时进行类型检查。结果是程序中使用某个名字之前,必须先告知编译器该名字的类型。
C++中最重要的特征是类(class)
,程序员可以使用类自定义数据类型。C++中这些类型有时也称为”类类型(class type)
“以区别语言的内置类型。
C++的主要设计目标之一就是允许程序员自定义类型,而且这些类型和内置类型一样易于使用。
C++语言定义了几种
基本类型
:字符型
、整型
、浮点型
等。C++还提供了可用于自定义数据类型
的机制,标准库
正是利用这些机制定义了许多更复杂的类型比如可变长字符串string
、vector
等。还能修改已有的类型以形成复合类型。
基本内置类型
C++定义了一组表示整数
、浮点数
、单个字符
和布尔值
的算数类型(arithmetic type)
。还定义了一种称为void
的特殊类型
。void类型
:void类型没有对应的值,仅用在有限的一些情况下,通常用作无返回值函数
的返回类型。算数类型
的存储空间大小
依机器而定。这里的大小是指用来表示该类型的二进制位(bit)数
。
C++标注规定了每个算数类型的最小存储空间,但他并不阻止编译器使用更大的存储空间。事实上,对于int类型,几乎所有的编译器使用的存储空房间都比所要求的大。(如表)
C++ 算数类型 | ||
---|---|---|
类型 | 含义 | 最小存储空间 |
bool | 布尔型 | - |
char | 字符型 | 8位 |
wchar_t | 宽字符型 | 16位 |
short | 短整型 | 16位 |
int | 整型 | 16位 |
long | 长整型 | 32位 |
float | 单精度浮点型 | 6位有效数字 |
double | 双精度浮点型 | 10位有效数字 |
long double | 扩展精度浮点型 | 10位有效数字 |
测试代码: |
1 |
|
输出:
(测试平台为windows 7 x64)
bool=1 byte
char=1 byte
wchar_t=2 byte
short=2 byte
int=4 byte
long=4 byte
float=4 byte
double=8 byte
long double=16 byte
整型
**整型
**:表示数字、字符和布尔值得算数类型。
**字符型
**:字符型有两种:char
和wchar_t
。char
类型保证了有足够的空间,能够存储机器基本字符集中任何字符相应的数值。因此,char类型通常是单个机器字节(byte)wchar_t
类型用于扩展字符集
,比如汉字和日文,这些字符集中的一些字符不能用单个char字符表示。
short、int和long类型都表示整型值,存储空间大小不同。(不同平台和架构其存储空间的大小也不同)char<short<=int<=long
(大于或等于依据编译器而定)bool类型
表示真值true
和false
。可将算数类型
的任何值赋值给bool对象
。0值算数类型
代表false。任何非0的值
都代表true。
带符号和无符号整型
除bool类型外,整型是可以带符号的(signed),也可以是无符号的。带符号类型
可以表示正数
也可以表示负数
(包括0),而无符号类型
则只能表示大于或等于0
的数。
注意:同一类型的数据范围unsigned
通常是signed
的两倍(因为signed
有一位为符号位
)。
重要:
整型
int
、short
、和long
都默认为带符号型
。要获得无符号型则必须指定该类型为unsignedunsigned int
类型可以简写为unsigned
,unsigned
后不加其他说明符就意味着是unsigned int
类型
char有三种不同的类型:char
、unsigned char
、signed char
.虽然char有三种不同的类型,但只有两种表示方式。可以使用unsigned char或signed char表示char类型。
unsigned char
:1255;127;signed char
:-127
不加修饰的char使用哪种表示方式由编译器决定。
整型值的表示
无符号型
中,所有的位都表示数值。
C++标准并未定义
signed类型如
何用位来表示,而是由每个编译器自由决定
如何表示signed类型,这些表示方式会影响signed的取值范围。
例:8位signed类型的取值肯定至少是从-127127,但也有许多实现允许从-128127.
表示signed整型类型
的最常见策略是用其中的一个位作为符号位
。符号位为1,值就为负数;符号位为0,值就为0或正数。
一个是用一位符号位的表示方式的8位signed整型取值是从-127~127
整型的赋值
对于unsigned类型
来说,编译器必须调整越界值
使其满足要求。编译器会将该值对unsigned类型的可能取值数目求模。然后取所得值。
例:8位的unsigned char,其取值范围是0~255(包括255).如果赋给超出这个范围的值,那么编译器将会取该值对256求模后的值。模运算
:”%”,取余。
注意:
对于unsigned类型来说,负数总是超出其取值范围。unsigned对象可能永远不会保存负数。有些语言中将负数赋值给unsigned对象是非法的,但是在C++中这是合法的。
C++中,把负值赋值给unsigned对象
是完全合法
的,其结果是该负数
对该类型
的取值个数
取模后的值。
例:把-1赋值给8位unsigned char,那么结果是255,因为255是-1对256求模后的值。
当将超过取值范围的值
赋值给signed类型
时,由编译器决定
实际赋值的值。在实际操作中,很多编译器处理signed类型的方式和unsigned类型类似。也就是说,赋值时是取该值
对该类型取值数目
求模后的值。**注意
**:然而我们不能保证编译器都会这样处理signed类型。
浮点型
类型float
、double
和long double
分别表示单精度浮点型
、双精度浮点型
和扩展精度浮点型
。
一般float用4字节表示(32bit)来表示,double类型用8字节(64bit)来表示,long double类型用12字节或16字节(96bit或者128bit)来表示。
注意:类型的取值范围
决定了浮点数所含有的有效数字位数
。
通常float类型的精度是不够的——float型只能保证6位有效数字。而double型至少可以保证10位有效数字,能够满足大多数计算机的需要。
建议:使用内置算数类型
- 当执行整型算数运算时,很少使用short类型
- 使用short类型可能会隐含赋值越界的错误。
- 比较典型的处理赋值越界(溢出)的情况是值
"截断(wrap around)"
以至于因越界而变成很大的负数。- 虽然char类型是整型,但是char型通常用来存储字符而不是用于计算。
在某些实现中char类型是整型sigend类型,在另外一则实现中则被当做unsigned类型,因此把char类型作为计算类型使用时容易出问题。
- 实际应用中,大多数通用机器都是使用和long类型一样长的32位来表示int型。
- 整型运算时,用
32位表示int类型
和64位表示long类型
的机器会出现选择int类型
还是long类型
的难题。在这些机器上,用long类型进行计算所付出的运行时代价远远高于用int类型进行同样计算的代价。- 浮点型:使用double类型基本不会有错。
在float类型中
隐式的精度损失
是不能忽视的,而双精度计算的代价相对于单精度可以忽略。事实上,有些机器上,double类型比float类型的计算要快得多。
long double类型提供的精度通常没有必要,而且还需要承担额外的运行代价。
字面值常量
像42这样的值被当作字面值常量,称之为字面值是因为只能用它的值称呼它,称之为常量是因为他的值不能修改。每个字面值都有相应的类型。
注意:只有内置类型存在字面值,没有类类型的字面值。因此也没有任何标准库类型的字面值。
整型字面值规则
定义字面值整数常量可以使用一下三种进制中的任一种:十进制、八进制和十六进制。
- 八进制:以0(零)开头的字面值整数常量表示八进制。如:024
- 十六进制:以0x或0X开头的表示十六进制。如:0x14
字面值整数常量的类型
默认为int
或long
类型。其精度类型决定于字面值——其值适合int就是int类型,比int大的值就是long类型。
通过增加后缀
,能够强制将字面值整数常量转换为long、unsigned或unsigned long类型。通过在数值后面加L或者l(大写或小写”L”)指定为long类型。
**注意
**:定义长整型时,用该使用大写字母L。小写字母l很容易和数值1混淆。
类似的,可通过在数值后面加U或U定义unsigned类型。同时加L和U能够得到unsigned long类型的字面值常量。
128u
/*unsigned*/
1024UL/*unsigned long*/
1L/*long*/
8Lu/*unsigned long*/
注意:没有short的字面值类型。
浮点字面值规则
可以使用十进制或者科学计数法来表示浮点字面值常量。使用科学计数法时,指数使用E或者e表示。
默认的浮点字面值常量位double类型。在竖直后面加上f
或者F
表示单精度同样加上L
或者l
表示扩展精度。
布尔字面值和字符字面值
布尔字面值
:单词true
和false
是布尔类型的字面值:bool test=true;
可打印的字符型字面
值通常用一对单引号
来定义:’a’、’b’、’c’
非打印字符的转义序列
不可打印的字符和特殊字符都用转义字符书写。转义字符都以反斜线符号()开始。
C++中定义了如下转义字符:
作用 | 转义字符 | 作用 | 转义字符 |
---|---|---|---|
换行符 | \n | 水平制表符 | \t |
回车符 | \r | 纵向制表符 | \v |
退格符 | \b | 进纸符 | \f |
反斜线 | \ | 单引号 | ' |
疑问号 | ? | 双引号 | " |
报警(响铃)符 | \a | 通用转义字符 | \ooo |
通用转义字符
:”\ooo”,这里ooo表示三个八进制的数字,这三个数字表示字符的数字值。
用
ASCII码字符集
表示字面值常量:
\7
(响铃符)\12
(换行符)\40
(空格符)\0
(空字符)\062
(‘2’)\115
(')
字符串字面值
字符串字面值
是一串常量字符
。
字符串字面值用双引号
括起来的零个
或者多个字符
表示。不可打印字符
表示成相应的转义字符
。
注意:为了兼容C语言,C++中所有的字符串字面值
都由编译器自动在末尾添加一个空字符
。
字符字面值
'A'
,表示单个字符;
然而,”A”,表示包含字母A和空字符两个字符的字符串。
宽字符字面值:L'a'
同样也存在宽字符串字面值,一样在前面加”L”:L"a wide string literal
(\0)
宽字符串字面值是一串常量宽字符
,同样以一个宽空字符结束。
变量
关键感念:
强静态类型
C++是一门静态类型语言
,在编译时会作类型检查
。对象的类型
限制了对象可以执行的操作。如果某种类型不支持某种操作,那么这种类型的对象也就不能执行该操作。(例:a%b,a、b皆为整型)
在C++中,操作是否合法是在编译时
检查的。当编写表达式时,编译器检查表达式中的对象是否按该对象的类型定义的使用方式使用。
静态类型检查能帮助我们更早的发现错误。静态类型检查使得编译器必须能识别程序中的每个实体。因此,程序中使用变量前必须先定义变量的类型。
什么是变量
变量提供了供程序可操作的有名字的存储区。
C++中每一个变量都有特定的类型。该类型决定了变量的内存大小
和布局
、能够存储于该内存中的值的取值范围
以及可应用在该变量上的操作集
。
C++程序员常常把变量称”变量”、”对象(object)”。
左值和右值
(1)左值
(lvalue,发音ell-value):左值可以出现在赋值语句的左边或右边。(变量)
(2)右值
(rvalue,发音are-value):右值只能出现在赋值的右边
,不能出现在赋值语句的左边
。(常量)
1 | //units_sald,sales_price,total_revenue是变量(左值),可以出现在赋值语句左边。 |
注意:有些操作符,比如赋值,要求其中一个操作数必须是左值。结果,可以使用的左值上下文比右值更广。左值出现的上下文决定了左值是如何使用的。
例如,表达式:
1 >units_sold = units_sold + 1;在该表达式中,units_sold变量被用作两种不同操作符的操作数。
+操作符只关心其操作数的值。变量的值是当前存储在和该变量相关联的内存中的值。加法操作符的值是取得变量的值并加1。
变量units_sold也被用作**=操作符的左操作数。
**=操作符获取右操作数并写入到左操作数。在这个表达式中,加法运算结果被保存到与units_sold相关联的存储单元中,而units_sold之前的值则被覆盖。
术语:什么是对象?
对象是内存中具有类型的区域。我们可以自由地使用对象描述程序中可操作的大部分数据,而不管这些数据是内置类型还是类类型,是有名字的还是没有名字的,是可读的还是可写的。
变量名
变量名,即变量的标示符(identifier)**,可以由字母
、数字
和下划线
组成。
**变量名必须以字母或者下划线开头,并且区分大小写字母:C++中的标示符都是大小写敏感的。
语言本身并没有限制变量名的长度,但考虑到将会阅读或修改我们代码的其他人,变量名不应太长。
C++关键字
C++保留了一组词用该语言的关键字。关键字不能用作成程序的标识符。
C++还保留了一些词用作操作符的替代名。这些替代名用于支持某些不支持标准C++操作符号集的字符集。他们也不能用作标识符。
注意:除关键字外,C++还保留了一组标识符用于标准库。标识符不能包含两个连续的下划线,也不能以下划线开头后面紧跟一个大写字母。有些标识符(在函数外定义的标识符)不能以下划线开头。
变量命名习惯
- 变量名一般用小写字母。例如:通常会写成index,而不是写成Index或者INDEX。
- 标识符应使用能帮助记忆的名字,也就是说,能够提示其在程序中用法的名字,如on_loan或salary。
- 包含多个词的标识符书写为在每个词之间添加一个下划线,或者内嵌的词的第一个字母都大写。例如:通常会写成student_loan或studentLoan,而不写成studentloan。
- 命名习惯最重要的是保持一致。
定义对象
每个定义都是以类型说明符(type specifier)**开始,后面紧跟着以逗号分开的含有一个或多个说明符的列表。** 分号结束定义。类型说明符指定与对象相关联的类型。
类型决定了分配给变量的存储空间和可以在其上执行的操作。
多个变量可以定义在同一条语句中:
1 | double salary,wage; |
初始化
变量定义指定了变量的类型和标识符,也可以为对象提供初始值。定义时指定了初始值的对象被称为是已初始化的(initialized)。
C++支持两种初始化变量的形式:
- **复制初始化(copy-initialized):**复制初始化语法用等号(=)。
int ival = 1024;
- **直接初始化(direct-initialized):**直接初始化式把初始化式放在括号中。
int ival(1024);
注意:在C++中理解“初始化不是赋值”是必要的,初始化和赋值时两种不同的操作。
初始化和赋值的区别:初始化指创建变量并给它赋初始值,而赋值则是擦除对象的当前值并用新值代替。
当初始化类类型对象时,复制初始化和直接初始化之间的差别是很微妙的。在后面的复制控制一章会详细解释他们的差别。直接初始化语法更加灵活且效率高。
使用多个初始化式
对内置类型的初始化只有一种方法:提供一个值,并把这个值复制到新定义的对象中。对内置类型来说,复制初始化和直接初始化几乎没有区别。
对类类型的对象来说,有些初始化仅能用直接初始化完成。
每个类都会定义一个或者几个特殊的成员函数来高速我们如何初始化类类型的变量。定义如何进行初始化的成员函数称为构造函数(coustructor)。和其他函数一样,构造函数能接受多个参数。一个类可以定义几个构造函数,每个构造函数必须接受不同数目或者不同类型的参数。
以string类为例。string类型在标准库中定义,用于存储不同长度的字符串。使用string时必须包含string头文件。和IO类型一样,string是定义在命名空间中的。
string定义了几种构造函数,使得我们可以用不同的方式初始化string对象。其中一种初始化string对象的方式是作为字符串字面值的副本:
1 | std::string titleA="C++ Primer"; |
以上两种初始化方式都可以使用。两种定义都创建了一个string对象,其初始值都是指定度字符串字面值的副本。
也可以通过一个计数器和一个字符串初始化string对象。这样创建的对象包含重复多次的指定字符,重复次数由计数器指定:
1 | //all_nines="9999999999" |
我们将在顺序容器一节详细解释string对象。
初始化多个变量
当一个定义中定义了两个以上的变量的时候,每个变量都可能有自己的初始化式。对象的名字立即变成可见,所以在同一个定义中前面已经定义变量的值初始化后面的变量。
1 | //直接初始化,wage被初始化为10000 |
已初始化变量和未初始化变量可以在同一个定义中定义。两种形式的文法可以相互混合。
1 | int interval,month = 11,day = 11,year = 1994; |
对象也可以用任意复杂度的表达式(包括函数的返回值)来初始化:
1 | double price = 109.99,discount = 0.16; |
上例中函数apply_discount
接受两个double
类型的值并返回一个double
类型的值。将变量price
、discount
传递给函数,并且用它的返回值来初始化sale_price
.
变量初始化规则
当定义没有初始化的变量时,系统有时候会帮我们初始化变量。这时,系统提供什么样的值取决于变量的类型,也取决于变量定于的位置。
内置类型变量的初始化
内置变量是否自动初始化取决于变量定义的位置。
- 在函数体外定义的变量都初始化成0
- 在函数体里定义的内置类型变量不进行自动初始化。
- 除了用作赋值操作符的左操作数,未初始化的变量用作任何其他用途都是未定义的。
**注意:**未初始化变量引起的错误难以发现。永远不要依赖未定义行为。
建议每个内置类型的对象都要初始化。虽然这样并不总是必须的,但是会更加容易和安全,除非你能确定忽略初始化式不会带来风险。
类类型变量的初始化
类通过定义一个或多个构造函数来控制对类对象的初始化。
默认构造函数(default constructor)
如果定义某个类的变量时没有提供初始化式,这个类也可以定义初始化时的操作。它是通过定义一个特殊的构造函数即**默认构造函数(default constructor)**来实现的。这个构造函数之所以被称作”默认构造函数”,是因为它是”默认”运行的。
如果没有提供初始化式,那么就会使用默认构造函数。不管变量在哪里定义,默认构造函数都会被使用。
大多数类都提供了默认构造函数。如果类具有默认构造函数,那么就可以在定义该类的变量时不用显式地初始化变量。
例如:
string
类定义了默认构造函数来初始化string变量为空字符串,即没有字符的字符串。
1
2 >//string对象empty是个空字符串
>std::string empty;
声明和定义
C++程序通常由许多文件组成。为了让多个文件访问相同的变量,C++区分了声明和定义。
声明:有关键字extern且没有初始化。
变量的定义(definition):用于为变量分配内存空间,还可以为变量指定初始值。在一个程序中,变量有且仅有一个定义。
声明(declaration):用于向程序表明变量的类型和名字。
定义也是声明:当定义变量时我们声明了它的类型和名字。可以通过extern关键字声明变量而不定义它。
不定义变量的声明包括对象名、对象类型和对象类型前的关键字extern:
1 | //声明但不定义i |
extern声明不是定义,也不分配存储空间。事实上,它只是说明变量定义在其他地方。程序中变量可以声明多次,但只能定义一次。
只有当声明也是定义时,声明才可以有初始化式,因为只有定义才分配存储空间。
初始化式必须要有存储空间来进行初始化。如果声明初始化式,那么它可被当做定义,即使声明标记为extern:
1 | //定义 |
只有当extern声明位于函数外部时,才可以含有初始化式。**(包括main()函数)**
因为已初始化的extern声明被当做定义,所以该变量任何随后的定义都是错误的。
1 | //定义 |
同样,随后的含有初始化式的extern声明也是错误的。
1 | //定义 |
声明和定义之间的区别可能看起来微不足道,但事实上却是举足轻重的。
任何在多个文件中使用的变量都需要有与定义分离的声明。在这种情况下,一个文件含有变量的定义,使用该变量的文件则包含该变量的声明(而不是定义)。
名字的作用域
C++程序中,每个名字都与唯一的实体(比如变量、函数和类型等)相关联。
用来区分名字不同意义的上下文称为**作用域(scope)**。作用域是程序的一段区域。一个名称可以和不同作用域中的不同实体相关联。
C++语言中,大多数作用域是用花括号来界定的。一般来说,名字从声明点开始直到其声明所在的作用域结束处都是可见的。
1 |
|
- 全局作用域(global scope):定义在所有函数外。(Ten)
- 局部作用域(local spoce):定义在函数内。(sum)
- 语句作用域(statement scope):定义在某个语句/块中。(val)
C++中作用域可嵌套
定义在全局作用域中的名字可以在局部作用域中使用。定义在全局作用域中的名字和定义在函数的局部作用域中的名字可以再语句的作用域中使用。
1 | //全局作用域变量Ten和局部作用域变量sum都可以在语句作用域中使用 |
当全局变量和局部变量的变量名相同时,局部变量会屏蔽全局变量
1 |
|
说明局部作用域变量index并没有在for循环中用到,被语句作用域变量index屏蔽了。
在语句章节将详细讨论语句作用域,在函数章节将讨论局部作用域和全局作用域。
C++还有另外两种不同级别的作用域:
- 类作用域(class scope):将在”类和数据抽象”部分中的类章节介绍。
- 命名空间作用域(namescape scope):将在”高级主题”中的命名空间章节介绍。
在变量使用处定义变量
变量的定义或声明可以放在程序中能摆放语句的任何位置。变量在使用前必须先声明或定义。
通常把一个对象定义在它首次使用的地方是一个很好的办法。
在对象第一次使用的地方定义对象可以提高程序可读性。读者不需要返回到代码段的开始位置去寻找某一特殊变量的定义,而且,在此处定义变量,更容易给他赋以有意义的初始值。
放置声明的一个约束是,变量只能在从其定义处开始到该声明所在的作用域的结果处才可以访问。必须在使用该变量的最外层作用域里面定义变量。
const限定符
1 | for(int index = 0;index != 512;++index){ |
上面的代码有两个小问题:
- 程序的可读性,比较index和512有什么意思?循环再在做什么?512意义何在?
- 程序的可维护性,在程序非常庞大时,不能安全快速的修改字面值常量数据。
解决这两个问题的办法是使用一个初始化为512的对象:
1 | int bufsize = 512; |
通过使用好记的名字以增加程序的可读性。
现在是对对象bufsize测试而不是对字面值常量512测试:index != bufsize;
如果想要改变bufsize的大小就不用查找和改正512出现过得地方,只需要修改初始化式的部分即可。
定义const对象
定义一个变量代表某一常数的方法仍然有一个严重的问题。即变量是可以被修改的。该代表常数的变量可能有意或无意的被修改。
const限定符提供了一个解决办法,它把一个对象转换成一个常量。
1 | const int bufsize = 512; |
定义bufsize为常量并初始化为512。变量bufsize仍然是一个左值,但是现在这个左值是不能修改的。任何修改bufsize的尝试都会导致编译错误。
1 | //因为常量在定义后就不能被修改,所以定义时必须初始化 |
const对象默认为文件的局部变量
在全局作用域里定义非const变量时,它在整个程序中都可以访问。
除非特别说明,在全局作用域声明的const变量是定义该对象的文件的局部变量。此变量只存在那个文件中,不能被其他文件访问。
通过指定const变量为extern,就可以在整个程序中访问const对象。
1 | //file_1.cc |
注意:非const变量默认为extern。要使const变量能够在其他文件中访问,必须显式地指定它为extern.
引用
- **引用(reference)**就是对象的另一个名字。在实际应用中,引用主要用作函数的形式参数。
- 引用是一种**复合类型(compound type)**,通过在变量名前添加”&”符号来定义。
- 复合类型是指用其他类型定义的类型。在引用的情况下,每一种引用类型都是”关联到”某一其他类型。
- 不能定义引用类型的引用,但可以定义任何其他类型的引用。
1 | //引用必须用与该引用同类型的对象初始化 |
引用是别名
因为引用只是他绑定对象的另一名字,作用在引用身上的操作事实上都是作用在该引用绑定的对象上。
1 | //对引用操作 //效果等同于 |
类似的:
1 | //把和ival相关联的值赋值给变量ii |
当引用初始化后,只哟该引用存在,他就保持绑定到初始化时指向的对象。不可能将引用绑定到另一个对象。(具有唯一性)
因为对引用的操作都直接作用在与该引用相关联的对象上,所以在定义引用时必须进行初始化。初始化时指明指向哪个对象的唯一方法。
定义多个引用
可以在一个类型中定义多个引用。必须在每个引用标示符前加”&”符号。
1 | int i=1024,i2=2048; |
const引用
const引用是指向const对象的引用。不能通过修改引用对象而修改被引用对象。
1 | const int ival = 1024; |
注意:将普通引用绑定到const对象上是不合法的
const引用可以初始化为不同类型的对象或者初始化为右值。
1 | //如字面值常量 |
但是同样的初始化用在非const上是不合法的,而且可能会导致编译错误。
1 | double ival=3.14; |
看到这里或许会奇怪,不是说引用只是对象的另一个名字吗?那为什么修改了对象的值,相对应的引用的值却没有被修改?
这时先要看将引用绑定到不同的类型时所发生的事:
1
2
3
4
5
6
7
8 >//假如我们编写以下代码
>double dval = 3.14;
>const int &refdval=dval;
>//编译器会把这些代码转换成如下形式的编码
>double dval = 3.14;
>//将double型变量赋值给int型变量会发生强制转换(double->int)
>int temp = dval;
>const int &refdval = temp;因为dval不是const,那么可以给他赋值一新值。这样做不会修改强制转换时使用的变量temp,所以也不会修改绑定到temp上的引用的值。
- 非const引用只能绑定到与该引用同类型的对象。
1 | int ival = 1024; |
- const引用则可以绑定到不同但相关的类型的对象或绑定到右值上。(提防const绑定到不同类型时的强制类型转换导致引用绑定的对象可以修改但不会作用到const引用上)
1 | //如字面值常量(右值) |
typedef名字
typedef可以用来定义类型的名字:
1 | typedef double wages;//wages是double的一个同义词 |
typedef名字可以用作类型说明符:
1 | wages hourly,weekly;//double hourly,week; |
typedef定义以关键字typedef开始,后面是数据类型和标识符。标识符或类型名没有引入新的类型,而只是现有数据类型的同义词。
typedef名字可以出现在程序中类型名可出现的任何位置。
typedef通常被用于以下三种目的:
- 为了隐藏特定类型的实现,强调使用类型的目的。
- 简化复杂的类型定义,使其更容易理解。
- 允许一种类型用于多个目的,同时使得每次使用该类型的目的明确。
用typedef简化函数指针的定义
函数指针相当的冗长。使用typedef为指针类型定义同义词,可将函数指针的使用大大简化。
1 | typedef bool (*cmpFcn)(const string &,const string &); |
该定义表示cmpFcn是一种指向函数的指针类型的名字。该指针类型为”指向返回bool类型并带有两个const string引用形参的函数的指针”。只要使用这种函数指针类型时,只需直接使用cmpFcn即可,不必每次都把整个类型声明全部写出来。
枚举
我们经常需要为某些属性定义一组可选择的值。例如,文件打开的状态可能会有三种:输入、输出和追加。记录这些状态的值的一种方法是使每种状态都与一个唯一的常数相关联。
1 | //我们可能会这样编写代码 |
**枚举(enumeration)**提供了一种方法,不但定义了整数常量集,而且还把他们聚集成组。
定义和初始化枚举
枚举的定义包括关键字enum,其后是一个可选的枚举类型名,和一个用花括号括起来、用逗号分开的**枚举成员(enumerator)**列表。
1 | //input is 0,output is 1,append is 2 |
默认的,第一个枚举成员赋值为0,后面的每个枚举成员赋的值比前面的大1。
枚举成员是常量
可以为一个或多个枚举成员提供初始值,用来初始化枚举成员的值必须是一个**常量表达式(constant expression)**。
常量表达式是编译器在编译时就能够计算出来结果的表达式。
整型字面值是常量表达式,就像一个通过常量表达式自我初始化的const对象也是常量表达式一样。
可以定义下列枚举类型:
1 | //shape is 1,sphere is 2,cylinder is 3,polygon is 4 |
枚举成员值可以是不唯一的:
1 | //point2d is 2,point2w is 3,point3d is 3,point3w is 4 |
注意:不能修改枚举成员的值。枚举成员本身就是一个常量表达式,所以也可以用于需要常量表达式的任何地方。
每个enum都定义一种唯一的类型
每个enum都定义了一种新的类型。和其他类型一样,可以定义和初始化Points类型(上例中创建的枚举类型)的对象,也可以以不同的方式使用这些对象。
枚举类型额对象初始化或赋值,只能通过其枚举成员或同一枚举类型的其他对象来进行。
1 | //testA is 0,testB is 1,testC is 2 |
类类型
C++中,通过定义**类(class)**来自定义数据类型。类定义了该类型的对象包含的数据和该类型的对象可执行的操作。
从操作开始设计类
每个类都定义了一个接口(interface)**和一个实现(implementation)。
**接口由使用该类的代码需要执行的操作组成。实现一般包括该类所需要的数据。实现还包括定义该类需要但又不供一般性使用的函数。
定义类时,通常先定义该类的接口,即该类所提供的操作。通过这些操作,可以决定该类完成功能所需要的数据,以及是否需要定义一些函数来支持该类的实现。
例如,我们将定义的类型所支持的操作:
- 加法操作符,将两个Sales_item对象相加
- 输入和输出操作符,读和写Sales_item对象
- 赋值操作符,把一个Sales_item对象赋给另一个Sales_item对象
- same_isbn函数,检测两个对象是否指同一本书
我们将会在函数章节和类和数据抽象部分的重载操作符与转换章节来介绍怎么来定义这些操作。
思考这些操作必须要实现都功能,可以看车该类需要什么样的数据。
Sales_item必须包含:
- 记录特定书的销售册数
- 记录该书的销售总数
- 计算该书的平均售价
通过上面的任务可以知道需要一个unsigned类型的对象来记录销售册数,一个doublu类型的对象来记录总销售收入,然后可以用总收入除以销售册数计算出平均售价。因为还要知道记录的是哪本书,所以还需要定义一个string对象来记录书的ISBN
定义类
我们需要能定义一种包含我们设计类的数据元素和在之前用到的操作的数据类型。在C++语言中,定义这种数据类型的方法就是定义类。
1 | class Sales_item{ |
类定义以关键字class开始,其后是该类的标识符。类体位于花括号里面。花括号后面必须要跟一个分号。
编程中忘记类定义后面的分号,是很普遍的的错误!!
类体可以为空。类定义了组成该类型的数据和操作。这些操作和数据是类的一部分,也称为类的成员(member)**操作称为成员函数,而数据称为数据成员**。
成员函数(member funtion):成员函数是由类定义的函数,有时称为类方法(method)**。
成员函数只定义一次,单被视为每个对象的成员。
当调用成员函数时,(通常)指定函数要操作的对象。语法是使用点操作符(.)**
点操作符通过它的左操作数取得右操作数。
点操作符仅用于类类型的对象:左操作数必须是类类型的对象,右操作数必须指定该类型的成员。
**注意:**点操作符(.)的右操作数不是对象或值,而是成员的名字。
类也可以包含0个到多个private或者public访问标号(access label)**。访问标号**控制的是类的成员在类的外部是否可以访问。使用该类的代码只能访问public成员
定义了类,也就是定义了一种新的类型。类名就是该类型的名字。
每个类都定义了它自己的作用域。也就是说,数据和操作的名字在类的内部必须唯一,但可以重用定义在类外的名字。
定义类的数据成员
定义类的数据成员和定义普通变量有些相似。
同样是指定一种类型并给该成员一个名字:
1 | //当定义Sales_item类型的对象时,这些对象将包含一个string型变量,一个unsigned型变量和一个double型变量 |
类的数据成员定义了该对象的内容。
定义变量和定义数据成员存在着非常重要的区别:
- 一般不能把类成员的初始化作为其定义的一部分。
- 当定义数据成员时,只能指定该数据成员的名字和类型。
- 类不是在类定义里定义数据成员时初始化数据成员,而是通过称为构造函数的特殊成员函数控制初始化。
访问标号
访问标号负责控制使用该类的代码是否可以使用给定的成员。
类的成员函数可以使用类的任何成员,而不管其访问级别。
访问标号private、public可以多次出现在类定义中。给定的访问标号应用到下一个访问标号出现时为止。
类中public部分定义的成员在程序的任何地方都可以访问。一般把操作放在public部分,这样程序的任何代码都能执行这些操作。
不是类的组成部分的代码不能访问private成员。这样可以保证对类对象进行操作的代码不能直接操纵其数据成员。
使用struct关键字
C++支持struct关键字,它也可以定义类类型。struct关键字是从C语言继承过来的。
如果使用class关键字来定义类,那么定义在第一个访问标号前的任何成员都隐式指定为private;如果使用struct关键字,那么这些成员都是public。
可以等效地定义Sales_item类为:
1 | struct Sales_item{ |
struct的成员默认都是public,除非有其他特殊的声明,所以就没有必要添加public标号。
用class和struct关键字定义类的唯一差别在于默认访问级别。
默认情况下,struct的成员为public,而class的成员为private。
编写自己的头文件
一般类定义都会放入头文件。C++程序使用头文件包含的不仅仅是类定义。
回想一下,名字在使用前必须先声明或定义。由多个文件组成的程序需要一种方法连接名字的使用和声明。在C++中这是通过头文件来实现的。
为了允许把程序分成独立的逻辑块,C++支持所谓的**分别编译(separate compilation)**。这样程序可以由多个文件组成。
设计自己的头文件
头文件为相关声明提供了一个集中存放的位置。头文件一般包含类的定义、extern变量的声明和函数的声明。
头文件的正确使用能够带来两个好处:
- 保证所有文件使用给定实体的同一声明
- 当声明需要修改时,只有头文件需要更新
设计头文件应该注意以下几点:
- 头文件所做的声明在逻辑上应该是适于放在一起的
- 编译头文件需要一定的时间
- 如果头文件太大,程序员可能不愿意承受包含该头文件所带来的编译时代价。
头文件用于声明而不是定义
当设计头文件时,记住定义和声明的区别是很重要的。定义只可以出现一次,而声明则可以出现多次。
下列语句是一些定义,所以不应该放在头文件中:
1 | extern int ival = 100;//虽然有extern关键字,但是有初始化,所以该语句是定义。 |
同一个程序中有两个以上文件含有上述任何一个定义都会导致多重定义链接错误。
注意:因为头文件包含在多个文件中,所以不应该含有变量或者函数的定义。
对于头文件不应该含有定义这一规则有三个例外:
- 可以定义类
- 值在编译时就已知的const对象
- inline函数(内联函数)
一些const对象定义在头文件中
const变量默认时是定义该变量的文件的局部变量。
当const整型变量通过常量表达式自我初始化时,这个const整型变量就可能是常量表达式。而const变量要成为常量表达式,初始化式必须为编译器可见。为了能够让多个文件使用相同的常量值,const变量和它的初始化式必须是每个文件可见的。而要使初始化式可见,一般都把这样的const常量表达式定义在头文件中。这样,无论该const变量何时使用,编译器都能看见其初始化式。
C++中任何变量都只能定义一次。定义会分配内存空间,而所有对该变量的使用都关联到同一存储空间。因为const对象默认认为定义它的文件的局部变量,所以把他们定义在头文件中是合法的。
以上行为有一个重要的含义:
当我们在头文件中定义了const变量后,每个包含头文件的源文件都有了自己的const变量,其名称和值都一样。
当该const变量是用常量表达式初始化时,可以保证所有的变量都有相同的值。但是在实践中,大部分编译器在编译时都会使用相应的常量表达式来替换这些对const变量的使用。所以,实践中不会有任何存储空间用于存储常量表达式初始化的const变量。
如果const变量不是用常量表达式初始化,那他就不应该在头文件中定义。相反,和其他变量一样,该const变量应该在一个原文件中定义并初始化。应在头文件中为他添加extern声明,以使其能被多个文件共享。
预处理器的简单介绍
#include
是C++预处理器(preprocessor)**的一部分。
预处理器处理程序的源代码,在编译之前运行。
**include指示只接收一个参数:头文件名。预处理器用指定的头文件内容替代每个#include
头文件经常需要其他头文件
头文件经常需要#include
其他头文件。例如Sales_item类的头文件必须包含string库,Sales_item类含有一个string类型的数据成员,因此必须可以访问string头文件。
包含头文件非常司空见惯,甚至一个头文件被多次包含也不稀奇。例如,使用Sales_item头文件的程序也可能使用饿了string库。在这种情况下,string被包含了两次:一次通过程序本身直接包含,另一次通过包含Sales_item头文件而间接包含。
设计头文件时应使其可以多次被包含在同一源文件中,这一点很重要。我们必须保证多次包含同一头文件不会引起该头文件定义的类和对象被多次定义。
使得头文件安全的通用做法是使用预编译器定义的**头文件保护符(header guard)**,其作用是避免在已经见到头文件的情况下重新处理该头文件的内容。
避免多重包含
预处理器允许我们自定义变量。
预处理器变量的名字在程序中必须是唯一的。任何与预处理器变量相匹配的名字的使用都关联到该预处理器变量。
为了避免名字冲突,预处理器变量经常用全大写字母表示。
预处理器变量有两种状态:已定义或未定义。定义预处理器变量和检测其状态所用的预处理器指示不同。
#define
指示接受一个名字并定义该名字为预处理器变量。#ifndef
指示检测指定的预处理器变量是否未定义。如果未定义那么跟在其后的所有指示都被处理,直到出现#endif
。
可以使用这些设施来预防多次包含同一文件:
1 |
注意:头文件应该含有保护符,即使这些头文件不会被其他头文件包含。编写头文件保护符并不困难,而且如果头文件被包含多次,它可以避免难以理解的编译错误。
使用自己的头文件
#include
指示接受以下两种方式:
1 |
- 如果头文件是在尖括号(<>)里,那么认为该头文件是标准头文件。编译器将会在预定位置集查找该文件(PATH路径)。
- 如果头文件名括在一对引号(“”)里,那么认为它是非系统头文件,非系统头文件的查找通常始于源文件所在的路径。