您的位置:首页 > 编程语言 > C语言/C++

《C++ Primer》读书笔记第2章:变量与基本类型

2017-06-13 14:57 323 查看

2.1 基础内置类型

c++ 定义了一套包括算数类型和空类型(void)在内的基本数据类型。算数类型分为两类:整形和浮点型。

c++包含的算数类型如下表所示

类型含义最小尺寸
bool布尔类型未定义
char字符8位
wchar_t宽字符16位
char16_tunicode字符16位
char32_tunicode字符32位
short短整型16位
int整形32位
long长整形32位
long long长整型64位
double双精度浮点数64位,10位有效数字
float单精度浮点数32位,6位有效数字
long double扩展精度浮点数96位,10位有效数字
对于上面这些内置类型,需要注意一下几点:

bool类型的取值为false和true,其通常被实现为8位,而且可以与整形互相转换。但是理论上bool类型实现为单字节也是可以的,比如STL中就有类似的标准库,其用一个字节来表示bool类型。

bool char 均属于整形类型,一定要注意这一点,今后所说到的整形的性质,对于bool与char类型也是适用的。

浮点数均具有有效数字,由于计算机中是以二进制存储浮点数,所以其无法做到绝对精确,因此对于浮点数的等于判断也要十分注意比如两个float类型的值分别为3.14159265333333与3.14159265333334,然而它们是可能相等的。所以在使用浮点数时,要注意保持其值在有效的位数之内。由于精度原因,浮点数通常也不是用==进行比较,而使用如下的方式:if(fabs(f1 - f2) < 0.000001)

除去bool和char 类型,其他整形可以划分为带符号的和无符号的两种。要特别注意此两种的整数的范围。并且字符型也被分为三种:char, signed char, unsigned char.

当从无符号数中减去一个值时,我们要确保结果不是一个负数,比如
for(uint8_t i = 10; i >=0; -- i)
. //当i 为 0时,继续递减,其不会变为负值,而是变为一个很大的正整数,而i >=0将永远为true,这是一个死循环。

切勿混用带符号整形与无符号整形,因为在计算中带符号整形会自动转换为无符号整形。比如int i = -1;unsiged int u = 1; i*u的计算中i会被转换为无符号整形,从而变为了一个很大的无符号整数,这样表达式的结果将出人意料。

7.为了程序的兼容性,通常在实际编程中并不会直接使用int,因为在不同的环境下int可能有不同的字节数,所以这将导致int的取值范围发生变化,所以在实际编程中通常会使用具有固定位数的int类型,比如int8_t,int16_t等。

2.2 变量

2.2.1 变量定义

初始值

当对象在创建时获得了一个值,我们说这个对象被初始化了,如下:

double price = 109.99;


一次性定义多个变量时,对象的名字随着定义也马上可以被使用,所以,在同一条定义语句中,可以使用先定义的变量初始化后定义的变量。

double price = 109.99, discount = price * 0.16


初始化和赋值虽然均使用“=”操作符,但是它们是两个不同的概念。初始化的含义是创建一个变量时赋予其一个初始值,而赋值的含义是把对象的当前值擦除,而以一个新值代替。

列表初始化

在c++ 11 标准中,增加了列表初始化,即使用一对花括号来包含初始值:

int unit_sold = 0;
int uint_sold {0};


当用于内置变量时,这种初始化方式有一个重要特点:如果我们使用列表初始化且初始化存在丢失信息的风险,则编译器会报错:

long double ld = 3.14159
int a{ld};  //错误,存在丢失信息的风险(小数点后被截断)
int b = ld;     //正确


默认初始值

如果一个变量定义时未被初始化,则其会被默认初始化,默认初始值由变量定义的位置决定。定义于任何函数体之外的变量会被初始化为0值,定义于函数体内部的内置变量将不被初始化,其值是未定义的,使用未初始化的内部变量将造成无法预计的后果。

2.2.2 变量声明与定义的关系

C++支持分离式编译机制,该机制允许将程序分割为若干个文件,每个文件被独立编译。为了支持分离式编译,c++将声明和定义区分开来。声明使得名字为程序所知,定义负责创建与名字关联的实体。如果想声明一个变量而非定义它,就在名字前加上关键字extern,而且不要显式的初始化。

extern int i;   //只声明不定义
int j;      //声明并定义


我们能给extern变量一个初始值,这样它也变成了一个定义。

extern int i = 0;    //声明并定义


变量能且仅能被定义一次,但是可以被多次声明。

如果在多个文件中使用同一变量,就必须将定义与声明分离开,变量的定义只能出现在一个文件中,而其他用到该变量的文件必须对其进行声明,却不能重复定义。

静态类型

C++是一种静态类型语言,其含义是会在编译阶段检查类型(称为类型检查),与之对应的lua,js等为动态类型语言。

2.2.3 标识符

c++为标准库保留了一些关键字,用户自定义的标识符不能连续出现两个下划线,以不能以下划线紧接大写字母。函数体外的标识符不能以下划线开头。

2.2.4 名字的作用域

一般来说,在对象第一次被使用的地方定义它是一个好习惯,这样便于程序的理解。内层作用域变量会屏蔽

同名的外层作用域变量。

2.3 复合类型

复合类型是指基于其他类型定义的类型,c++有几种复合类型,本章主要介绍其中两种:引用和指针。通常复合类型的声明由一个基本类型和一个紧随其后的声明符列表组成。


2.3.1引用

c++11中新增了右值引用,这种引用主要用于内置类,在这里我们介绍的是左值引用。
引用并非对象,它只是为一个已存在的对象所起的另一个名字。定义引用时,程序把引用和它的初始值绑定在一起,而不是将初始值拷贝给引用。一旦初始化完成,引用将和它的初始值对象一直绑定在一起。因为无法令引用绑定到另一个对象,所以引用必须初始化。因为引用本身不是一个对象,所以不能定义引用的引用,但是可以将一个引用作为另一个引用的初始值。


int ival = 1024;
int &refVal1 = ival;        //将引用refval1绑定到对象ival上
int &refVal2;               //错误,引用必须进行初始化
int &refVal2 = refVal1;     //将引用refVal1作为refval2的初始值


允许在一条语句中定义多个引用,其中每一个变量都必须要有相应的标识符。除了某些特殊情况外(主要是右值引用),其他所有的引用类型都要和相应的对象严格绑定。而且引用只能绑定在对象上,而不能与字面值或某个表达式的计算结果绑定在一起。


int r = 0;
int &r1 = r, &r2 = r1;      //定义两个引用,后一个引用以前一个引用为初值
int &r3 = r, r4 = r3, *r5 = &r4;    //分别定义了int&,int,int*三种类型。
double d = 3.14;
int &r6 = d;            //错误,不能把int类型的引用绑定到double类型上,类型必须严格匹配


2.3.2 指针

指针与引用

指针是指向另外一种类型的复合类型。

指针与引用有两个不同点:

其一,指针本身就是一个对象,允许对指针赋值和拷贝,而且也可以在运行时使其指向不同的对象。

其二,指针无需再定义时赋初值。

指针值

指针值应属于一下四种状态

指向一个对象

指向紧邻对象所占空间的下一个位置(通常用于超出末端)

空指针,意味着指针没有指向任何对象

无效指针,也就是除上述三种情况外的其他值

试图拷贝或以其他方式方式访问无效指针的值都将导致未定义行为。

空指针

c++11中引入nullptr来表示空指针,其是一个std::nullptr_t类型值。

现在c++应尽量使用nullptr,而不用NULL或者0来表示空指针。

建议用nullptr初始化所有指针,以防止使用无效的指针造成为定义行为。在任何函数内的指针如何未指定初始值,则进行默认初始化变为无效指针,而在全局中的指针会用nullptr进行初始化。

void *指针

void*是一种特殊的指针类型,可以存放任意对象的地址。其通常会用来作为函数的参数来传递各种类型的数据。任何类型的指针均可以转换为void *类型

int i = 3;
int *p1 = &i;
void *pv = static_cast<void *>(p1);
int *p2 = static_cast<int *>(pv);
cout << *p2 << endl;


2.3.3理解复合类型的声明

很多程序员容易迷惑于基本数据类型和类型修饰符的关系,其实后者不过是声明符的一部分罢了。如:

int *p1,p2;


其中基本数据类型是int而非int*, *仅仅只是修饰了p而已,对改声明语句中其他变量,它不产生任何作用。 由于指针是一个对象,所以存在指向指针的引用

int i = 42;
int *p = &i;
int *&r = p;


要理解r的类型到底是什么,最简单的方法是从右到左阅读r的定义,离变量名最近的符号对变量名有最直接的影响,因此上述代码中r为一个引用,引用的类型为int *类型。

2.4 const限定符

  c++中使用const对变量的类型加以限定,把其定义成一个常量。const对象一旦创建后其值就不能再改变,所以const对象必须进行初始化。

const int i = get_size();   //运行时初始化,要特别注意此种情况
const int i = 42;           //编译时初始化
const int k;                //错误,const变量必须进行初始化


  对于const对象只能执行不改变其内容的操作。但是如果利用一个对象去初始化另一个对象,则他们是不是const都无关紧要

int i = 42;
const int ci = i;
int j = ci;             //正确


默认情况下,const对象仅在文件内有效

  默认情况下,const对象被设定为仅在文件内有效,单多个文件出现同名const变量是,其实等同于在不同的文件中分别定义了独立的变量。

  为了使各多个文件之间可以共享const对象,必须在const的变量定义之前添加extern关键字,最常用的写法如下:

//file_1.h
extern const int bufsize;           //声明bufsize为外部变量
//file_1.cpp
extern const int bufsize = fcn();   //定义const变量并使其可以被外部变量共享


2.4.1 const的引用

  可以把引用绑定到const对象上,我们称之为常量的引用。常量的引用可以绑定到非const对象上,但是非常量引用不可以绑定到const对象上

int i = 1
const int ci = 2;
const int &r1 = ci;     //正确,const引用可以绑定到const对象上
int &r2 = ci;           //错误,非const引用不能绑定在const对象上
const int &r3 = i;      //const引用可以绑定到非const对象上,但不可以通过此引用改变对象的值


  通常引用类型必须与其所引用的对象类型一致,但是初始化常量引用时可以用任意表达式作为初始值,而且可以为一个常量引用绑定非常量的对象,字面量,甚至是一个一般表达式。

int i = 42;
const int &r1 = i;
const int &r2 = 42;
const int &r3 = r1 *2;      //正确,可以用任意表达式对常引用进行初始化
const int &r4 = 3.14;       //正确,可以用double类型的字面量来初始化int的常引用
int &r5 = r1 *2;        //错误,非常量引用的初始值必须是左值


2.4.2 指针与const

  const与指针可以有以下组合:

int i = 4;
const int ci = 5;
int *p1 = &i;               //指向非常量的非常指针
const int *p2 = &i;         //指向常变量的非常指针,可以指向一个非常变量,但不可以通过此指针改变此变量的值
const int *p3 = &ci;        //指向常变量的非常指针,指向了一非常变量
int *p4 = &ci;              //错误,指向非常变量的指针不能指向常变量对象
int *const p4 = &i;         //指向非常变量的常指针,可以
c558
同过此指针改变其指向对象的值,但不可以改变指针自身的值
int * const p4 = &ci;       //错误,指向非常变量的指针不能指向一个常变量
const int * const p5 = &i;  //指向常变量的常指针,可以指向一个非常变量
const int *const p5 = &ci;  //指向常变量的常指针,可以指向一个常变量


2.4.3 顶层const

  如上所见,当一个指针指向一个对象时,由于指针也是一个对象,所以指针和指针所指向的对象均可以用const来修饰。c++ 11中,用名词顶层const 表示指针本身是一个常量,而用底层const表示指针所指向的对象是一个常量。一个指针类型既可以是顶层const也可以是底层const。

  当执行对象的拷贝操作时,拷入和拷出的对象必须具有相同的底层const,或者两个对象的数据结构必须能够转化,一般来说,非常量可以转换为常量,反之则不行。

2.4.4 constexpr 和常量表达式

注:constexpr在vs2013中并未受到支持,vs2015才开始支持此特性

constexpr 变量

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

const int max_files = 20            //max_files是常量表达式
const int limit = max_files + 1;    //由于max_files是常量表达式,所以limit也是常量表达式
int static_size = 27;               //非const对象,非常量表达式
const int sz = get_size();          //并不是用常量表达式初始化,sz也不是常量表达式


  c++11新标准规定,允许将变量声明为constexpr类型以便由编译器来验证变量的值是否是一个常量表达式。声明为constexpr的变量一定是一个常量,而且必须用常量表达式初始化。

constexpr int mf = 20;
constexpr int limit = mf + 1;
constexpr int sz = size();      //只有在size()是一个常量表达式时此声明语句才正确


  新标准允许定义一种特殊的constexpr函数,这种函数应该足够简单以使得编译时就可以计算其结果,这样就能用ocnstexpr函数去初始化constexpr变量了。

//constexpr函数
constexpr int sz(int i)
{
return i * 2;
};
const int i = 10;
int arr[sz(i)];                 //传入的这个参数也必须是一个constexpr类型


指针与constexpr

  果在constexpr声明中定义了一个指针,那么constexpr限定符只对指针对象有用,而与指针所指的对象无关。其中的关键在于constexpr把它所定义的对象置为顶层const。

const int *p1= nullptr;             //p1为一个指向整形常量的指针
constexpr int *p2 = nullptr;        //p2为一个指向整数的常量指针,p2既可以指向一个常量,也可以指向一个非常量


字面值类型

  算数类型、引用、指针都属于字面值类型。

  一个constexpr指针的初始值必须是nullptr,或者是存储于固定地址中的对象。函数体内定义的变量一般都不具有固定地址,相反,定义域所有函数体之外的对象其地址固定不变,可用来初始化constexpr指针。

2.5 类型处理

2.5.1 类型别名

类型别名是一个名字,它是某种类型的同义词。

传统的方法是用关键字typedef定义,新标准规定了一种新的方法,使用别名声明。

typedef double wages;
using wages = double;
typedef void (funcTest)(int *p);        //定义了函数指针funcTest
using funcTest = void (int *p);         //同上


指针,常量与类型别名

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

typedef char *pstring;
const pstring cstr = nullptr;
const pstring *ps;


  在上述代码中,const pstring 就是指向char的常量指针,而不是指向常量字符的指针,一定要注意这一点。

而ps是一个指向指针的指针类型,其所指的对象为一个执行那个char的常量指针。

2.5.2 auto 类型说明符

  c++ 引入了auto类型说明符,auto让编译器通过初始值来推算变量的类型。显然,auto定义的变量必须要有初值。

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

int i,*j,&k = i;
auto i = 0, *j = &i, &k = i;        //必须每个都要有初值,并且要保持基本数据一致
auto i= 0, pi = 3.14;               //错误,两个变量的基础类型不一致


复合类型、常量与auto

  当用auto推断引用类型时,并不会推断出引用类型,而是以引用所关联的对象的类型作为auto的类型。

int i = 0, &r = i;
auto a = r;         //最后a的类型为int类型而不是int &类型


  其次,当auto一般会忽略顶层const,而保留底层const,而如果希望推断出的auto类型是一个顶层const,则需要明确指出:

int i = 0, &r = i;
const int ci = i, &cr = ci;
auto b = ci;    //b是一个整数,顶层const被忽略
auto c = cr;    //c是一个整数,顶层const被忽略
auto d = &i;    //d是一个指向整数的指针
auto e = &ci;   //e是一个指向整数常量的指针,底层const被保留
auto &g = ci;   //g是一个整形常量引用,底层const被保留
const auto f = ci;      //ci的推断类型为const,此声明语句明确指出f的类型为const int


2.5.3 decltype 类型指示符

  c++11新引入了decltype标识符,其作用是选择并返回操作数的数据类型。

  decltype与auto的不同点在于,delctype不会忽略类型顶层const,而且其也可以推断出引用类型。

const int ci = 0, &cj = ci;
decltype(ci)  x = 0;            //x类型为const int,顶层const不会被忽略
decltype(cj) y = x;             //y类型为const int &,可以推断出引用类型
decltype(cj) z;                 //错误,z为const int &类型,必须进行初始化


  另一方面,如果表达式的内容是解引用,则decltype将得到引用类型。因为,解引用得到的是指针指向的对象,这与对象的引用类似。

int i = 42, *p = &i. &r = i;
decltype(r + 0) b;  //b类型为int类型
decltype(r) z;      //错误,z类型为int &,必须初始化
decltype(*p) c;     //错误,表达式内容为解引用,c类型int &,必须初始化


  decltype与auto的另一个重大区别是,decltype的结果类型与表达式形式密切相关

  如果decltype使用的是一个不加括号的变量,而得到的结果将是该变量的类型,而如果变量加上一层或多层括号,变量此时被作为复制语句左值的特殊表达式,所以这样将得到引用类型。

int i = 0;
decltype(i) a;      //a类型为int类型
decltype((i)) b;    //错误,b类型为int&类型,必须进行初始化


  切记:decltype((variable))的结果永远是引用,而decltype(variable)的结果只有当variable的类型为引用时,推断的类型才会是引用。(这真是一个很让人无语的坑!!!)
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: