天天看点

【C++】C++基础 —— 变量和基本类型

参考资料:C++ Primer 中文版(第5版)——[美] Stanley B. Lippman [美] Josée Lajoie [美] Barbara E. Moo 著 王刚 杨巨峰 译

代码编辑器:VS 2019

文章目录

    • 1. 基本内置类型
      • 1.1 算术类型
      • 1.2 类型转换
      • 1.3 字面值常量
        • 1.3.1 整型和浮点型字面值
        • 1.3.2 字符和字符串字面值
        • 1.3.3 布尔字面值和指针字面值
    • 2. 变量
      • 2.1 变量的定义
        • 2.1.1 列表初始化
        • 2.1.2 默认初始化
      • 2.2 名字的作用域
    • 3. 复合类型
      • 3.1 引用
      • 3.2 指针
    • 4. const 限定符
      • 4.1 const 的引用
      • 4.2 指针和 const
      • 4.3 顶层 const
      • 4.4 constexpr 和常量表达式
        • 4.4.1 常量表达式
        • 4.4.2 constexpr 变量
        • 4.4.3 字面值类型
        • 4.4.4 指针和 constexpr
    • 5. 处理类型
      • 5.1 类型别名
      • 5.2 auto 类型说明符
      • 5.3 decltype 类型指示符
    • 6. 自定义数据类型

1. 基本内置类型

C++ 定义了一套包括算术类型(arithmetic type)和空类型(void)在内的基本数据类型。其中,算术类型包含了字符、整型数、布尔值和浮点数。

1.1 算术类型

其中常用的那些类型(

int

char

float

double

long

short

long long

等)和 C 中的一样,不再赘述。布尔类型是 C 中没有的,C++ 中的布尔类型的取值为

true

false

1.2 类型转换

类型转换:

  • 算术值 0 赋给布尔类型时,结果为

    false

    ,否则结果为

    true

  • 布尔值

    true

    赋给非布尔类型,结果为 0 ,

    false

    则结果为 1。
  • 浮点数赋给整数,结果仅保留浮点数中小数点之前的部分。
  • 赋给无符号类型一个超出它表示范围的值时,结果是初始值对无符号类型表示数值总数取模后所得的余数。
  • 当我们赋给带符号类型一个超出它表示范围的值时,结果是未定义的(undefined)。此时,程序可能继续工作、可能崩溃,也可能生成垃圾数据。

1.3 字面值常量

一个形如 42 的值被称作字面值常量(literal),这样的值一望而知。每个字面值都对应一种数据类型,字面值常量的形式和值决定了它的数据类型。

1.3.1 整型和浮点型字面值

整型字面值可以写作十进制数、八进制数和十六进制数的形式。

类型 表示形式
十进制 常规表示方法
八进制 以 0 开头
十六进制 以 0x 或 0X 开头

默认情况下,十进制字面值是带符号数,八进制和十六进制字面值既可能是带符号的也可能是无符号的。

尽管整型字面值可以存储在带符号数据类型中,但严格来说,十进制字面值不会是负数。负号仅仅是对字面值取负而已。

浮点型字面值默认是

double

型的,可以以小数形式表示,也可以用指数形式表示。

1.3.2 字符和字符串字面值

由单引号括起来的一个字符称为

char

型字面值,双引号括起来的零个或多个字符串构成字符串型字面值。

注:一个字符也包括若干转义字符。

1.3.3 布尔字面值和指针字面值

true

false

是布尔类型的字面值。

nullptr

是指针字面值,代表空指针。

2. 变量

2.1 变量的定义

变量的声明不再赘述,下面重点放在初始化上。

2.1.1 列表初始化

C++ 语言定义了初始化的好几种不同形式,这也是初始化问题复杂性的一个体现。例如:

//我们想要给 int 型变量 a 赋初值为2,以下的4条语句都可以实现:
int a = 2;
int a = {2};
int a{2};
int a(2);
           

用花括号来初始化变量即为列表初始化。当用于内置类型的变量时,这种初始化有一个重要特点:如果我们使用列表初始化且初始值存在丢失信息的风险,则编译器将会报错,比如:

long double pi = 3.1415926535;
int a{pi};//报错,因为存在丢失信息的风险
int b(pi), c = pi;//正确,执行类型转换,且确实丢失了部分值
           

2.1.2 默认初始化

如果定义变量时没有指定初值,则变量被默认初始化,此时变量被赋予了”默认值“。默认值到底是什么由变量类型决定,同时定义变量的位置也会对此有影响。

每个类各自决定其初始化对象的方式。而且,是否允许不经初始化就定义对象也由类自己决定。如果类要求每个对象都必须显式初始化,此时创建对象不做初始化操作将引发错误。

2.2 名字的作用域

不论是在程序的什么位置,使用到的每个名字都会指向一个特定的实体:变量、函数、类型等。然而,同一个名字如果出现在程序的不同位置,也可能指向的是不同实体。

作用域(scope)是程序的一部分,在其中名字有其特定的含义。C++ 语言中大多数作用域都以花括号分隔。

建议:当你第一次使用变量时再定义它。这样做有助于更容易地找到变量地定义。更重要的是,当变量的定义离它第一次被使用的地方很近时,我们也会赋给它一个比较合理的初始值。

如果函数有可能用到某全局变量,则不宜再定义一个同名的局部变量。

3. 复合类型

复合类型(compound type)是指基于其他类型定义的类型。C++ 语言有几种复合类型,这里介绍其中的两种:引用和指针。

除了变量名组成的比较简单的声明符外,还有更复杂的声明符,它基于基本数据类型得到更复杂的类型,并把它指定给变量。

3.1 引用

C++11 中新增加一种引用:所谓的"右值引用(rvalue reference)",这种引用主要用于内置类。
严格来说,当我们使用术语"引用(reference)"时,指的其实是"左值引用(lvalue reference)"。
           

引用(reference)为对象起了另外一个名字,引用类型引用(refers to)另外一种类型。通过将声明符写成

&d

的形式来定义引用类型,其中

d

是声明的变量名。例子:

int num = 10;
int &renum = num;   // renum 指向 num (是 num 的另一个名字)
int &renum2;        // 报错:引用必须被初始化
           

一般在初始化变量时,初始值会被拷贝到新建的对象中。

然而定义引用时,程序把引用和它的初始值绑定(bind)在一起,而不是将初始值拷贝给引用。一旦初始化完成,引用将和它的初始化对象一直绑定在一起。因为无法令引用重新绑定到另外一个对象,因此引用必须初始化。

定义了一个引用之后,对其进行的所有操作(赋值,获取值,作为另一个变量的初始值等等)都是在与之绑定的对象上进行的。

因为引用本身不是一个对象,所以不能定义引用的引用。

允许在一条语句中定义多个引用,其中每个引用标识符都必须以符号

&

开头。

除了两种例外情况,其他所有引用的类型都要和与之绑定的对象严格匹配。两种例外情况是:

  • 第一种例外情况(跳转至例子):在初始化常量引用时允许用任意表达式作为初始值,只有该表达式的结果能够转换成引用的类型即可。
  • 第二种例外情况:对于存在继承关系的类,我们可以将基类的指针或引用绑定到派生类对象上。这就有一层极为重要的含义:当使用基类的引用(或指针)时,实际上我们并不清楚该引用(或指针)所绑定对象的真实类型。该对象可能是基类的对象,也可能是派生类的对象。
int &renum = 10;	//错误:引用类型的初始值必须是一个对象
double dnum = 3.14;
int &renum2 = dnum;	//错误:此处引用类型的初始值必须是 int 型对象
           

3.2 指针

指针(pointer)是“指向(point to)”另外一种类型的复合类型。与引用类似,指针也实现了对其他对象的间接访问。然而,指针与引用相比又有很多不同点:

  • 其一,指针本身就是一个对象(引用本身并不是对象),允许对指针赋值和拷贝,而且在指针的生命周期内它可以先后指向几个不同的对象。
  • 其二,指针无须在定义时赋初值。定义指针时如果没有被初始化,它也拥有一个不确定的值。

除了两种例外情况外,其他所有指针的类型都要和它所指向的对象严格匹配。两种例外情况:

  • 第一种例外情况:允许令一个指向常量的指针指向一个非常量对象。
  • 第二种例外情况:同上面引用的第二种例外情况。点我跳转

指针的值应属于下列4种状态之一:

  1. 指向一个对象。
  2. 指向紧邻对象所占空间的下一个位置。
  3. 空指针,意味着指针没有指向任何对象。
  4. 无效指针,也就是上述情况之外的其他值。

试图拷贝或以其他方式访问无效指针都将引发错误。编译器并不负责检查此类错误,这一点和试图使用未经初始化的变量是一样的。访问无效指针的后果无法预计,因此程序员必须清楚给定的指针是否有效。

空指针的几种生成方法:

int *pi = nullptr;		//等价于 int *p1 = 0;
int *p2 = 0;			//直接将 p2 初始化为字面常量0

//在有 #include cstdlib 的情况下,可以使用下面的语句:
int *p3 = NULL;			//等价于 int *p3 = 0;
           

int 变量不能直接赋给指针!即使 int 变量的值恰好等于 0 也不行!

建议:初始化所有指针。

任何非零指针对应的条件值都是

true

void *

是一种特殊的指针类型,可用于存放任意对象的地址。一个

void*

指针存放着一个地址,这一点和其他指针类似。不同的是,我们对该地址中到底是个什么类型的对象并不了解:

利用

void *

指针能做的事比较有限:拿它和别的指针比较、作为函数的输入和输出,或者赋给另外一个

void *

指针。不能直接操作

void *

指针所指的对象,因为我们并不知道这个对象到底是什么类型,也就无法确定能在这个对象上做哪些操作。

引用本身不是一个对象,因此不能定义指向引用的指针。但指针是对象,所以存在对指针的引用:

int i = 1;
int *p;
int *&r = p;	//r 是一个对指针 p 的引用

r = &i;			//r 引用了一个指针,因此给 r 赋值 &i 就是令 p 指向 i
*r = 0;			//解引用 r 得到 i,也就是 p 指向的对象,将 i 的值改为0
           

4. const 限定符

有时我们希望定义这样的一种变量,它的值不能被改变。例如用一个变量来表示缓冲区的大小。使用变量的好处是当我们觉得缓冲区大小不再合适时,很容易对其进行调整。另一方面也应随时警惕防止程序一不小心改变了这个值,为了满足这一要求,可以用关键字

const

对变量的类型加以限定。

const

对象一旦创建后其值就不能再改变,所以

const

对象必须初始化。初始值可以是任意复杂的表达式。

利用一个对象去初始化一个

const

对象时,前者是不是

const

类型都无关紧要。

默认状态下,

const

对象仅在文件内有效,要想在文件间共享,需要使用

extern

关键字。为了方便,我们对于

const

变量不管是声明还是定义都添加

extern

关键字,这样只需定义一次就可以了,即在一个文件中定义

const

,而在其他多个文件中声明并使用它。

4.1 const 的引用

可以把引用绑定到

const

对象上,就像绑定到其他对象上一样,我们称之为对常量的引用(reference to const)。与普通引用不同的是,对常量的引用不能被用作修改它所绑定的对象。

前面提到引用的两个例外,这里便出现了第一种例外的情况。在初始化常量引用时允许用任意表达式作为初始值,只要该表达式的结果能转换成引用的类型即可。引用的第一种例外的例子:

//例子
  int i = 1;
  const int &r1 = i;
  const int &r2 = 1;		 //正确:r1是一个常量引用
  const int &r3 = r1 * 2;  //正确:r3是一个常量引用
  const int &r4 = r1 * 2;  //错误:r4是一个普通的非常量引用
           

要弄清楚这种例外发生的原因,就要弄清楚当一个常量引用被绑定到另外一种类型上时到底发生了什么:

double dval = 3.14;
const int &ri = dval;
           

上面这两个语句是可以执行的,而且输出

ri

引用的值发现等于3。发生了什么呢?事实上,为了确保让

ri

绑定一个整数,编译器把上述代码变成如下形式:

double dval = 3.14;
const int temp = dval;
const int &ri = temp;
           

在这种情况下,

ri

绑定了一个临时量(temporary)对象。所谓临时量对象就是当编译器需要一个空间来暂存表达式的求值结果时临时创建了一个未命名的对象。C++ 程序员们常把临时量对象简称为临时量。

如果把语句改为:

double dval = 3.14;
int &ri = dval;		//错误:引用的类型与要绑定的对象的类型不一致
           

很容易想明白它非法的原因,因为这样会让

ri

绑定到一个临时量对象上,这样对

ri

进行赋值操作就没有任何意义了,所以C++将其归为非法。

4.2 指针和 const

与引用一样,也可以令指针指向常量或非常量。类似于常量引用,指向常量的指针(pointer to const)不能用于改变其所指对象的值。要想存放常量对象的地址,只能使用指向常量的指针。

前面提到,指针的类型必须与其所指对象的类型一致,但是有两个例外。第一种例外情况是允许令一个指向常量的指针指向一个非常量对象:

double dval = 3.14;
const double *cptr = &dval;		//正确:但是不能通过 cptr 改变 dval 的值
           

下面介绍一下 const 指针。

指针是对象而引用不是,因此就像其他对象类型一样,允许把指针本身定为常量。常量指针(const pointer)必须初始化,而且一旦初始化完成,它的值就不能再改变了(即它指的那个地址就不能再改变了)。把

*

放在

const

关键字之前用以说明指针是一个常量,这样的书写形式隐含着一层意味,即不变的是指针本身的值而非指向的那个值:

int num = 0;
int *const cpnum = #		//cpnum 将一直指向 num
const double pi = 3.14159;
const double *const pip = π	//pip 是一个指向常量对象的常量指针
/*
弄清楚这些声明的含义最行之有效的办法是从右向左阅读。
如第2行,int *const cpnum
const 代表 cpnum 本身是一个常量对象,对象的类型由声明符的其余部分确定, * 即代表是个指针,所以 cpnum 是一个常量指针,int 则代表常量指针对象指向的是一个 int 对象。

第4行的类似。
*/
           

4.3 顶层 const

如前所述,指针本身是一个对象,它又可以指向另外一个对象,因此指针本身是不是常量以及指针所指的是不是一个常量就是两个相互独立的问题。用名词顶层 const(top-level const)表示指针本身是个常量,而用名词底层 const(low-level const)表示指针所指的对象是一个常量。

更一般的,顶层 const 可以表示任意的对象是常量,这一点对任何数据类型都适用,如算术类型、类、指针等。底层 const 则与指针和引用等复合类型的基本类型部分有关。比较特殊的是,指针类型既可以是顶层 const 也可以是底层 const ,这一点和其他类型相比区别明显:

int i = 0;
int *const p1 = &i;		//不能改变 p1 的值,这是一个顶层 const
const int ci = 1;		//不能改变 ci 的值,这是一个顶层 const
const int *p2 = &ci;	//允许改变 p2 的值,这是一个底层 const
const int *const p3 = p2;//靠右的const是顶层 const,靠左的是底层const
const int &r = ci;		//用于声明引用的 const 都是底层 const
           

当执行对象的拷贝操作时,常量是顶层 const 还是底层 const 区别明显。其中,顶层 const 不受什么影响。执行拷贝操作并不会改变被拷贝对象的值,因此,拷入和拷出的对象是否是常量都没什么影响。

另一方面,底层 const 的限制却不能忽略。当执行对象的拷贝操作时,拷入和拷出的对象必须具有相同的底层 const 资格,或者两个对象的数据类型必须能够转换。一般来说非常量可以转换成常量,反之则不行:

int *p = p3;	//错误:p3 包含底层 const 的定义,而 p 没有
p2 = p3;		//正确:p2 和 p3 都是底层 const
p2 = &i;		//正确:int* 能转换成 const int*
int &r = ci;	//错误:普通的 int& 不能绑定到 int 常量上
const int &r2 = i;//正确:const int& 可以绑定到一个普通 int 上
           

4.4 constexpr 和常量表达式

4.4.1 常量表达式

常量表达式(const expression)是指值不会改变并且在编译过程就能得到计算结果的表达式。显然,字面值属于常量表达式,用常量表达式初始化的 const 对象也是常量表达式。

一个对象(或表达式)是不是常量表达式由它的数据类型和初始值共同决定。例如:

const int maxa = 20;	//是常量表达式
const int mina = maxa + 1;//是常量表达式
int num = 10;			//不是常量表达式
const int result = get_result();//不是常量表达式
           

对最后一个语句做一下解释:尽管

result

本身是一个常量,但它的具体值直到运行时才能获取到,所以也不是常量表达式。

4.4.2 constexpr 变量

在一个复杂系统中,几乎不可能分辨出一个初始值到底是不是常量表达式。将变量声明为

constexpr

类型以便由编译器来验证变量的值是否是一个常量表达式。声明为

constexpr

的变量一定是一个常量,而且必须用常量表达式初始化。例子:

constexpr int a = 20;		//20 是常量表达式
constexpr int b = a + 1;	//a + 1 是常量表达式

constexpr int c = size();	//只有当 size() 是一个 constexpr 函数时,才是一条正确的声明语句
           

注:

constexpr

函数应该足够简单以使得编译时就可以计算其结果,这样就能用

constexpr

函数去初始化

constexpr

变量了。

一般来说,如果认定了变量是一个常量表达式,那就把它声明成 constexpr 类型。
           

4.4.3 字面值类型

常量表达式的值需要在编译时就得到计算,因此对声明

constexpr

时用到的类型必须有所限制,因为这些类型一般比较简单,值也显而易见、容易得到,就把它们称为“字面值常量”(literal type)。

算术类型、引用和指针都属于字面值类型。自定义类、IO库、

string

类型则不属于字面值类型,也就不能定义成

constexpr

一个

constexpr

指针的初始值必须是

nullptr

或者

,或者是存储于某个固定地址中的对象。

函数体内定义的变量一般来说并非存放在固定地址中,因此

constexpr

指针不能指向这样的变量。相反的,定义于所有函数体之外的对象其地址固定不变,能用来初始化

constexpr

指针。

4.4.4 指针和 constexpr

constexpr

声明中如果定义了一个指针,限定符

constexpr

仅对指针有效,与指针所指的对象无关:

const int *p = nullptr;			//p 是一个指向整型常量的指针
constexpr int *q = nullptr;		//q 是一个指向整数的常量指针
           

constexpr

会把它所定义的对象置为了顶层

const

与其他常量指针类似,指针既可以指向常量,也可以指向一个非常量:

constexpr int *np = nullptr;	//np 是一个指向整数的常量指针,其值为空
int j = 0;
constexpr int i = 42;			//i 的类型是整型常量
//注意:i 和 j 必须定义在函数体之外,下面的语句才合法
constexpr const int *p = &i;	//p 是常量指针,指向整型常量 i
constexpr int *p1 = &j;			//p1 是常量指针,指向整数 j
           

5. 处理类型

5.1 类型别名

类型别名(type alias)是一个名字,它是某种类型的同义词。使用类型别名有很多好处,它让复杂的类型名字变得简单明了、易于理解和使用,还有助于程序员清楚地知道使用该类型的真实目的。

两种定义类型别名的方法:

//方法1,使用关键字 typedef
typedef double wages;	//wages 是 double 的同义词
typedef wages base, *p;	//base 是 double 的同义词,p 是 double* 的同义词

//方法2,使用别名声明,用关键字 using 作为别名声明的开始
using SI = Sales_item;	//SI 是 Sales_item 的同义词
//把等号左侧的名字规定成等号右侧类型的别名
           

如果某个类型别名指代的是复合类型或常量,那么把它用到声明语句里就会产生意想不到的后果。下面用例子说明:

typedef char *pstring;
const pstring cstr = 0;		//cstr 是指向 char 的常量指针
const pstring *ps;			//ps 是一个指针,它的对象是指向 char 的常量指针
           

两条声明语句的基本数据类型都是

const pstring

const

是对给定类型的修饰。

pstring

实际上是指向

char

的指针,因此,

const pstring

就是指向

char

的常量指针,而非指向常量字符的指针。

但如果直接错误地进行下面的理解:

替换后的语句的含义和之前的完全不同!替换后表达的含义是

cstr

是一个指向

const char

的指针。之所以产生这样的后果,是因为前后两个声明语句的基本数据类型发生了改变:用

pstring

声明时,基本数据类型是指针;替换后,基本数据类型是

const char

*

是声明符的一部分。

5.2 auto 类型说明符

使用

auto

类型说明符可以让编译器替我们去分析表达式所属的类型。和原来那些只对应一种特定类型的说明符不同,

auto

让编译器通过初始值来推算变量的类型。显然,

auto

定义的变量必须有初始值。

使用

auto

也能在一条语句中声明多个变量。因为一条声明语句只能有一个基本数据类型,所以该语句中所有变量的初始基本数据类型都必须一样:

auto i = 0, *p = &i;		//正确:i 是整数、p 是整型指针
auto sz = 0, pi = 3.14;		//错误:sz 和 pi 的类型不一致
           
  • 使用引用作为初始值时,编译器会以引用对象的类型作为

    auto

    的类型。
  • auto

    一般会忽略掉顶层

    const

    ,同时底层

    const

    则会保留下来,比如当初始值是一个指向常量的指针时:
    int i = 0;
    const int a = i, &ra = a;
    auto b = a;		//b 是一个整数(a 的顶层 const 特性被忽略掉了)
    auto c = ra;	//c 是一个整数
    auto d = &i;	//d 是一个整型指针(整数的地址就是指向整数的指针)
    auto e = &ci;	//e 是一个指向整数常量的指针(对常量对象取地址是一种底层 const)
               
  • 如果需要推断出的

    auto

    类型是一个顶层

    const

    ,需要明确指出:
    const int a = 0;
    const auto f = a;	//a 的推演类型是 int,f 是const int
               
  • 还可以将引用的类型设为

    auto

    ,此时原来的初始化规则仍然适用:
    const int a = 0;
    auto &g = a;		//g 是一个整型常量引用,绑定到 a
    auto &h = 1;		//错误:不能为非常量引用绑定字面值
    const auto &j = 1;	//正确:可以为常量引用绑定字面值
               
  • 要在一条语句中定义多个变量,切记,符号

    &

    *

    只从属于某个声明符而非基本数据类型的一部分,因此初始值必须是同一种类型:
    int i = 0;
    const int a = i;
    auto k = a, &l = i;		//k 是整数,l 是整型引用
    auto &m = a, *p = &a;	//m 是对整型常量的引用,p 是指向整型常量的指针
    auto &n = i, *p2 = &a;	//错误:i 的类型是 int 而 &a 的类型是 const int
               

5.3 decltype 类型指示符

decltype

类型说明符选择并返回操作数的数据类型,编译器并不实际计算表达式的值:

decltype

处理顶层

const

和引用的方式与

auto

有些许不同。如果

decltype

使用的表达式是一个变量,则

decltype

返回该变量的类型(包括顶层

const

和引用在内):

const int a = 0, &ra = a;
decltype(a) x = 0;		//x 的类型是 const int
decltype(ra) y = x;		//y 的类型是 const int&,y 绑定到变量 x
decltype(ra) z;			//错误:z 是一个引用,必须初始化
           

需要指出的是,引用从来都作为其所指对象的同义词出现,只有在

decltype

处是一个例外。

下面给出一个

decltype

和引用的例子:

//decltype 的结果可以是引用类型
int i = 10, *p = &i, &r = i;
decltype(r) a;		//正确:a 是 int& 型
decltype(r + 0) b;	//正确:b 是 int 型

//如果表达式是解引用操作,则 decltype 将得到引用类型
decltype(*p) c = i;	//正确:c 是 int& 型,也进行了初始化
decltype(*p) d;		//错误:d 是 int& 型,必须初始化
           

即表达式的内容是解引用操作时,

decltype

将得到引用类型。

从上面我们可以看出,

decltype

的结果类型与表达式密切相关,这是

decltype

auto

的重要区别之一。还有一种情况需要特别注意,对于

decltype

所用的表达式来说,如果变量名加上了一对括号,则得到的类型与不加括号时会有不同。如果

decltype

使用的是一个不加括号的变量,则得到的结果就是该变量的类型;如果给变量加上了一层或多层括号,编译器就会把它当成一个表达式。变量是一种可以作为赋值语句左值的特殊表达式,所以这样的

decltype

就会得到引用类型:

// decltype 的表达式如果是加上了括号的变量,结果将是引用
int i = 1;
decltype((i)) a;	//错误:a 是 int&,必须初始化
decltype(i) b;		//正确:b 是一个未初始化的 int
           

切记:

decltype((variable))

的结果永远是引用,而

decltype(variable)

结果只有当 variable 本身就是一个引用时才是引用。

6. 自定义数据类型

从最基本的层面理解,数据结构是把一组相关的数据元素组织起来然后使用它们的策略和方法。

C 里面的结构体在 C++ 中改称为类,是一种只有数据成员没有成员函数的类。不同的是,C++ 新标准规定可以为数据成员提供一个类内初始值。创建对象时,类内初始值将用于初始化数据成员。没有初始值的成员将被默认初始化。

提供类内初始值时,或者放在花括号里面,或者放在等号右边,但要注意不能用圆括号。

可以在函数体内定义类,但是这样的类毕竟受到了一些限制。所以,类一般都不定义在函数体内。当在函数体外部定义类时,在各个指定的源文件中可能只有一处该类的定义。而且,如果要在不同文件中使用同一个类,类的定义就必须保持一致。

为了确保各个文件中类的定义一致,类通常被定义在头文件中,而且累所在头文件的名字应与类的名字一样。例如,库类型

string

在名为

string

的头文件中定义。

头文件通常包含那些只能被定义一次的实体,如类、

const

constexpr

变量等。头文件也经常用到其他头文件的功能(所以就会存在重复包含的问题)。

下面给出重复包含头文件的解决方案:

比如我们的

Dog.h

中要用到

string

,所以包含了头文件

string.h

,在

my_dog.cpp

中我们也要用到

string

,所以再一次包含了

string.h

确保头文件多次包含仍能安全工作的常用技术是预处理器(preprocessor)。预处理器是在编译之前执行的一段程序,可以部分地改变我们所写的程序。C++ 程序还会用到的一项预处理功能是头文件保护符(header guard),头文件保护符依赖于预处理变量。预处理变量有两种状态:已定义和未定义。

#define

指令把一个名字设定为预处理变量,另外两个指令则分别检查某个指定的预处理变量是否已经定义:

#ifdef

当且仅当变量意定义时为真,

#ifndef

当且仅当变量未定义时为真。一旦检查结果为真,则执行后续操作直至遇到

#endif

指令为止。使用示例:

#ifndef STRING_H
#define STRING_H
#include<string>
#endif
           

Dog.h

my_dog.cpp

中要写

#include<string>

语句时,都按照上面这样修改,就可以解决重复包含的问题了。

还有一种更为方便的解决重复包含的方案(推荐使用这种),就是在文件最开头加上

#pragma once

,CSDN 上一篇讲解它的用法的:C/C++ 中#pragma once的使用。

下面给出上一篇文章中的

Dog

类的完整程序,共有三个源文件:

Dog.h

Dog.cpp

my_dog.cpp

//Dog.h
#pragma once
#include<string>
class Dog {
public:
    Dog(int dog_age, std::string dog_breed);//构造函数,创建实例时需要传入参数
    void roll();//打滚儿
    int get_age();//输出年龄
    std::string get_breed();//输出品种
private:
    int age;//年龄
    std::string breed;//品种
};
           
//Dog.cpp
#pragma once
#include"dog.h"
#include<iostream>
#include<string>

Dog::Dog(int dog_age, std::string dog_breed){
	age = dog_age;
	breed = dog_breed;
}

void Dog::roll() {
	std::cout << "The dog is rolling over!" << std::endl;
}

int Dog::get_age() {
	return age;
}

std::string Dog::get_breed() {
	return breed;
}
           
#pragma once
#include<iostream>
#include"dog.h"

int main() {
	Dog my_dog(3, "Teddy");
	std::cout << "My dog is " << my_dog.get_age() << '.' << std::endl;
	std::cout << "The breed of my dog is " << my_dog.get_breed() << '.' << std::endl;
	my_dog.roll();
    return 0;
}
           

在 VS 2019 中以非调试模式运行项目,运行结果:

【C++】C++基础 —— 变量和基本类型

【C++】C++ 基础——变量和基本类型的内容到这里就结束了。

下一篇预告:【C++】C++ 基础——字符串、向量和数组

链接指路:

上一篇:【C++】开始

下一篇:【C++】C++ 基础——字符串、向量和数组

继续阅读