不同于其它语言比如C#,C++中有很多专有的术语,有些术语还十分重要,比如声明、定义、初始化,初始化又有直接初始化和赋值初始化,了解这些术语对于掌握C++非常重要,这里就简单记录一下:
为什么会有声明和定义的区分
C++语言支持分离式编译(seperation compilation)机制,即允许将程序分割为若干文件,每个文件可以被独立编译。如果将程序分为多个文件,则需要有在文件间共享代码的方法,一个文件中的代码可能需要另一个文件中定义的变量。
为了支持分离式编译,C++语言将声明(declaration)和定义(definition)区分开来。声明使得名字为程序所知,一个文件如果想使用别处定义的名字,必须包含对那个名字的声明。而定义则负责创建与名字关联的实体。
变量的声明规定的变量的名字和类型,定义也一样,但除此之外,定义还申请存储空间,也可能会为变量赋予一个初始值。我们通常看到的.h头文件和.cpp实现文件,通常用来存放声明和定义。
定义和声明的区别看起来似乎微不足道,但实际上非常重要,如果要在多个文件中使用同一个变量,则必须将声明和定义分离。变量的定义必须出现在且只能出现在一个文件中,其它用到改变了的文件,必须对其进行声明,绝对不能对其进行重复定义。
声明
声明(declaration)就是告诉编译器某个东西的名称和类型(type),但略去细节。
extern int x;
std::size_t numDigits(int number);
class Widget;
template <typename T>
class GrapheNode;
第一条表示声明一个名字为x的int类型,如果去掉extern,就变成了一个定义,并且给它赋予了一个默认的初始值。如果extern包含了初始值,那就不是声明,而变成了定义,比如:
extern int x; // 声明一个int类型的x
int y; // 定义一个int类型的y,并默认赋予初始值0
extern int z = 3; // 这里extern后面包含了初始值,变成了一个定义,而不是声明
extern int z=3,一般的编译器会给一个警告信息:
'z' initialized and declared 'extern'
第二条语句是一个函数的声明,也叫函数的签名(signature),它指明了函数的名称、参数和返回值。numDigits函数的签名是 std::size_t (int),表示“这个函数获得一个int,并返回一个size_t”。
第三条语句声明了一个名为Widget的类。如果该类在后续定义,则这个声明叫做前向声明(forward declaration)。前向声明的对象,在声明之后,定义之前是一个不完全类型(incomplete type),我们只知道该对象是一个类型,但不知道包含哪些成员。不完全类型只能用于定义指向该类型的指针,或声明使用该类型作为形参指针类型或返回指针类型的函数。因为指针类型对编译器而言大小固定(如32位机上为4字节),不会出现编译错误。
在前向声明和具体定义之间涉及标识符(变量、结构、函数等)实现细节的使用都是非法的(表明只能使用指针的形式来引用前向声明)。若函数被前向声明但未被调用,则编译和运行正常;若前向声明函数被调用但未被定义,则编译正常但链接报错(undefined reference)。将具体定义放在源文件中可部分避免该问题。
第四条语句声明了一个带有模板的类。
定义
定义(definition)的任务是提供给编译器声明(declaration)所遗漏的细节。对类对象而言,它是编译器为对象分配内存的起点。对函数(function)或者函数模板(function template)而言,定义提供了方法的具体实现。
int x;
std::size_t numDigits(int number)
{
std::size_t digitsSoFar = 1;
while ((number /= 10) != 0)
{
++digitsSoFar;
}
return digitsSoFar;
}
class Widget
{
private:
/* data */
public:
Widget(/* args */);
~Widget();
};
template <typename T>
class GrapheNode
{
private:
/* data */
public:
GrapheNode(/* args */);
~GrapheNode();
};
上面的代码,就是对声明中的4条语句的具体定义。
初始化
初始化(initialization)是“给与对象初值"的过程,对于用户自定义类型的对象,初始化是由构造函数来执行的。”初始化“和”赋值“在C++里面是由区别的。初始化不是赋值,初始化的含义是,创建变量时赋予其一个初始值,而赋值的含义是把对象的当前值擦除,用一个新的值来替代。
C++定义了初始化的好几种不同形式,比如定义一个名为unsold的int变量并把值初始化为0,可以有下面多种形式。
int unsold = 0;
int unsold = {0};
int unsold{0};
int unsold(0);
第三种,使用花括号初始化的方式时C++ 11新标准的一部分,被称为列表初始化(list initialization)。当用于内置对象的初始化时,列表初始化如果初始值存在丢失信息的风险,则编译器会报错。
long double ld = 3.1415926789;
int a{ld}, b = {ld}; // 编译器会发出警告,精度会丢失
int c(ld), d = ld; // 转换会执行,但是会丢失精度
上述第二行,编译器会给予错误提示:
narrowing conversion of 'ld' from 'long double' to 'int' [-Wnarrowing]
如果定义变量时,没有指定初始值,那么变量会被默认初始化(default initialized),变量会被赋予”默认值“,默认值是什么由变量类型决定,同时变量定义的位置也会有影响。
内置类型的默认初始化
内置类型的变量如果没有被显示初始化,则它的值由定义的位置决定。定义在任何函数之外的变量被初始化为0,定义在函数体内部的内置类型变量将不被初始化(uninitialized)。一个未被初始化的内置类型变量的值是未定义的。
#include <iostream>
int unsold;
int main()
{
int sold;
std::cout << unsold << std::endl;
std::cout << sold << std::endl;
return 1;
}
我这里输出结果是:
0
32759
自定义类型初始化
用户自定义类型对象的初始化由类的构造函数(constructor)执行。自定义类在初始化时,如果我们没有提供初始值,则该类会执行默认的初始化。类通过一个特殊的构造函数来控制默认的初始化过程,这个函数叫默认构造函数(default constructor)。默认构造函数没有任何实参,或者所有的实参都有缺省值。
如果类不显示定义构造函数,则编译器会为其生成一个合成的默认构造函数(synthesized default constructor),这个合成的默认构造函数将按照以下规则初始化类的数据成员:
- 如果存在类内的初始值,则用它来初始化成员
- 否则,默认初始化该成员。
class A
{
public:
A();
};
class B
{
public:
explicit B(int x = 0, bool b = true) : x(x), b(b)
{
}
public:
int x;
bool b;
};
class C
{
public:
explicit C(int x);
};
上面定义了几个自定义类,以及他们的默认构造函数,B和C的构造函数都被声明了explicit,这可以阻止它们被用来执行隐式类型转换转换,但仍然可以执行显示类型转换。除非有好的理由允许构造函数执行隐式类型转换,否则尽量声明为explicit。
void doSomething(const B &object)
{
std::cout << object.x << " " << object.b << std::endl;
}
int main()
{
B bObj1; // 默认初始化一个B类型对象,相当于调用B bObj1(0,true)
doSomething(bObj1); // 可以正常使用,穿第一个B对象给方法
B bobj2(27); // 正常,传一个int 27对象来初始化x
doSomething(28); // 编译器报错,不存在从“int”转为“B”的适当构造函数
doSomething(B(28)); // 正常
}
"如果extern包含了初始值,那就不是定义,而变成了声明" 看您后面的comment这句话会不会应该是 “那就不是声明,而变成了定义”?