如果一个声明(还)给出了声明的实体的完整描述的话,我们称之为定义。下面是一个定义的例子:
int a=7;
vector<double> v;
double sqrt(double d){/*...*/}
对比定义,我们按惯例用“声明”表示“不是定义的声明”。如:
int x = 7; //定义
extern int x; //声明
extern int x; //另一个声明
double sqrt(double); //声明
double sqrt(double d){/*...*/} //定义
double sqrt(double); //sqrt的另一个声明
double sqrt(double); //sqrt的再一个声明
int sqrt(double); //错误:sqrt不一致的声明
声明的类别:C++允许程序员定义很多类别的实体,我们比较关心的有:变量、常量、函数、名字空间、类型、模板。
变量和常量的声明,指定一个名字和一个类型,并可进行初始化。例如:
int a; //不带初始化
double d=7; //使用=语法初始化
vector<int> vi(10); //使用()语法进行初始化
vector<int> vi2 {1,2,3,4}; //使用{}语法进行初始化
常量的声明语法与变量一样,差别在于类型之前多了一个关键字const,而且必须初始化。
const int x=7; //使用=语法进行初始化
const int x2{9}; //使用{}语法进行初始化
const int y; //错误:未进行初始化
我们倾向于使用{}初始化语法。
我们通常不对string、vector等对象进行初始化。因为这些类型定义了默认初始化机制。vector对象的默认初始化值为空(不包含任何元素)。string对象的值为空串“”。
注意:最常使用的变量-局部变量和类成员,是不会被初始化的,除非对其进行初始化,在实践中一定要注意。
在C++中,对于“别处”定义的功能的声明,管理它们的关键是“头部”。本质上一个头部(header)是一些声明的集合,一般定义于一个文件,因此也称为“头文件”(header file)。这样的头文件随后用#include包含在我们的源文件中。
C++支持多种类型的作用域:
- 全局作用域:在任何其他作用域之外的程序区域。
- 名字空间作用域:一个名字空间作用域嵌套于全局作用域或另一个名字空间作用域中。
- 类作用域:一个类内的程序区域。
- 局部作用域:位于{...}大括号之间或函数参数列表中的程序区域。
- 语句作用域:例如for语句内的程序区域。
当已有的函数中有参数不再使用,但是已经有大量“外部”代码调用时使用该参数。当我们不希望重写这些代码时(不想更改函数声明)。我们可以不为其命名:
int my_find(vector<string> vs, string s, int)
{
for(int i=0;i<vs.size();++i)
if(vs[i] == s) return i;
return -1;
}
函数参数的传递方式包括:传值、传常量引用、传引用。我们的基本原则是:
- 使用传值方式传递非常小的对象。
- 使用传常量引用方式传递你不需要修改的大对象。
- 让函数返回一个值,而不是修改通过引用参数传递来的对象。
- 只有迫不得已才使用传引用方式。
在编译时对函数进行计算可以通过将函数声明为constexpr来实现。一个constexpr函数若给定常量表达式作为参数, 则函数计算将在编译时完成。
constexpr double xscale = 10; //缩放因子
constexpr double yscale = 0.8;
constexpr Point scale(Point p){return {xscale * p.x, yscale * p.y};};
C++11中,要求constexpr函数体只能包含一条return语句;在C++14中,还可以写简单的循环语句。一个constexpr函数不允许有副作用,即它不能改变函数体之外的变量值。以下是违反函数简单性规则的一个函数例子:
int gob = 9;
constexpr void bad(int& arg) //错误:没有返回值
{
++arg; //错误:通过参数修改了某些变量的值
gob = 7; //错误:修改了非局部变量
}
除非是在一些非常特殊的情况下,否则一般来说使用全局变量不是一个好主意。程序员没有有效方法获知程序的哪个部分读或写了一个全局变量。另一个问题是,在不同的编译单元中的全局变量的初始化顺序是不确定的。
//file1.cpp
int x1 = 1;
int y1 = x1 + 2;
//file2.cpp
extern int y1;
int y2 = y1 + 2; //y2为2或者5
如果确实需要使用一个全局变量(或常量),而且需要对它进行复杂的初始化时,一种常用的技术是编写一个函数,返回我们需要的初值。例如:
const Date default_date()
{
return Date(1970,1,1);
}
如果需要频繁调用的话,可以这样做:
const Date& default_date()
{
static const Date dd(1970,1,1); //第一次到达这里时初始化dd
return dd;
}
名字空间(namespace),即是无须定义一个类型就能将类、函数、数据和类型组织成一个可识别的命名实体。例如:
namespace Graph_lib{
struct Color {/*...*/};
struct Shape {/*...*/};
struct Line:Shape {/*...*/};
struct Function:Shape {/*...*/};
struct Text:Shape {/*...*/};
//...
int gui_main() {/*...*/};
}
由一个名字空间的名字(或一个类名)和一个成员名组合而成的名字称为全限定名。
一个一般性的原则是,除非是std这种在某个应用领域中大家已经熟悉的名字空间,否则最好不要使用using指令。