C++ Primer第二章 变量与基本类型

第二章 变量和基本类型

1.初始化

初始化不是赋值,初始化的含义是创建变量的时候赋予其一个初始值,而赋值的含义是把对象的当前值擦除,用一个新值来替代。

1.1 列表初始化

用花括号来初始化变量的形式称为列表初始化

  int x = {0};
  int x = 0;
  int x{0};
  int x(0);

以上四条语句都可以实现定义一个名为x的值并且初始化为0
列表初始化的特点:如果我们使用列表初始化且初始值存在丢失信息的风险,编译器将报错

long double PI = 3.1415926535;
int a{PI}, b = {PI};
int c(PI), d = PI;

以上代码中,a,b的初始化可能会被编译器拒绝

1.2默认初始化

  • 如果定义变量的时候没有指定初值,则变量会被默认初始化,默认的值由变量类型决定。
  • 对于内置类型来说,定于在任何函数体之外的变量会被初始化为0,定义在函数体内部的内置变量将不会被初始化。也就是它的值是未定义的,如果试图以拷贝或者其他形式访问它,会引发错误

2 分离式编译

  • 为了允许把程序拆分成多个逻辑部分来编写,C++支持分离式编译机制,该机制允许程序分割为若干个文件,每个文件被独立编译
  • 要做到分为多个文件,则需要有在文件之间共享代码的方法,例如一个文件的代码可能需要用到另一个文件的变量
  • 为了支持这点,C++将声明和定义区分开来
    • 声明:使得名字为程序所知,一个文件如果想使用别的地方定义的名字必须包含对那个名字的声明
    • 定义:负责创建和名字关联的实体
  • 相比于声明,定义会申请存储空间,也可能会为变量赋一个初始值
  • 在变量名前添加关键字extern,就可以声明一个变量。
  • 注意,声明是不需要显式的初始化变量的,extern关键字标记的变量会被赋一个初始值,如果显式的声明,就抵消了extern的作用.
extern int i; //声明
int i;   //定义
extern int i = 1; //定义
  • 变量只能被定义一次,但是可以被多次声明

3 静态类型

  • C++是一种静态类型语言,含义是在编译阶段检查类型,检查类型的过程称为类型检查
  • 由于对象的类型决定了对象能够参与的运算,在C++中,编译器会检查数据类型是否支持要执行的运算,如果执行类型不支持的运算,就会报错并且不会生成可执行文件
  • 类型检查有助于发现问题,前提是编译器必须知道每一个实体对象的类型,所以我们在使用变量前必须声明它的类型

4 指针

  • 指针的值(即地址)应属于以下四种状态之一
  1. 指向一个对象
  2. 指向紧邻对象所占的下一个位置
  3. 空指针
  4. 无效指针(上述情况以外的其他值)
  • 试图拷贝或者以其他方式访问无效指针的值都会引发错误,编译器不负责检查这类错误,和使用未初始化的变量一样,都会产生不可预计的后果

4.1 空指针

空指针不指向任何对象

//几个生成空指针的方法,这三者等价
int *p1 = nullptr;
int *p2 = 0;
int *p3 = NULL;

NULL 是过去的程序使用的预处理变量,由预处理器负责管理,当用到一个预处理变量时,预处理器会自动将它替换为实际值,因此使用NULL和0初始化指针是一样的,现在最好使用nullptr而不是NULL

4.2 void* 指针

  • void*是一种特殊的指针类型,可以存放任意对象的地址,但是我们对该地址中到底是个什么类型的对象并不了解
  • 利用void*指针能做的事情比较有限
    1. 拿它和别的指针比较
    1. 作为函数的输入输出
    1. 赋值给另外一个void指针
      不能直接操作void
      指针指向的对象,因为我们不知道这个对象到底是什么类型

4.3 指向指针的引用

int i = 42;
int *p;   //p是一个int指针
int *&r = p; //r是对指针p的引用
r = &i;  //因为r是引用,因此这句就是将p指向i
*r = 0;  //*r就是p指向的地方i,所以这句是把i的值改为0

5 const限定符

5.1 const对象仅在文件内有效

  • 当编译时以初始化的放视定义一个const对象时,例如const int bufSize = 512;,编译器将在编译过程中把用到该变量的地方都替换成对应的值,也就是说,编译器会找到代码中所有用到bufsize的地方,然后用512替换
  • 在这个过程中,编译器必须知道变量的初始值,但是如果程序包含多个文件,每个用到了const对象的文件都必须得能够访问它的初始值才可以,这就使得每一个用到该变量的文件都有它的定义。
  • 同时,为了避免同一个变量的重复定义,默认情况下,const对象被设定为仅在文件内有效,当多个文件出现同名const变量时,等同于不同文件中分别定义了独立的变量。
  • 如果我们想和使用非常量一样使用const,使得一个const变量可以在文件间共享,也就是在一个文件中定义const,别的文件通过声明使用它。方法是对于const变量不管声明还是定义都添加extern关键字,这样就只需要定义一次。
//定义并初始化了bufSize,因为他是一个常量,必须使用extern使其可以被其他文件使用
extern const int bufSize = fcn();
//此处下面的bufSize则是一个声明
extern const int bufSize;

5.2 const的引用

  • 对常量的引用不能呗用作修改它所绑定的对象
const int a = 1024;
const int &b = a; //引用及其对象都是常量
b = 42; //错误,b是对常量的引用
int &c = b; //错误,试图让非常量引用指向常量对象

因为不允许改变a的值,因此也不能通过引用的方式去改变它,所以引用他的也一定是常量

5.3 初始化和对const的引用

引用的类型必须与其所引用对象的类型一致,只有两个例外,这里说其中一个

  • 初始化常量引用允许用任意表达式作为初始值,只要该表达式能转换成引用的类型,允许为一个常量引用绑定非常量的对象、字面值甚至是一个一般表达式
int i = 42;
const int &a = i; //正确,允许将一个const int&绑定到普通int对象
const int &b = 42; //正确,b是一个常量引用
const int &c = a * 2; //正确,c是一个常量引用
int &d = a * 2; //错误,r4是一个非常量引用
  • 本质上来说,const int &c = a * 2 这个语句,为了确保c可以绑定到一个数,编译器采用的方法是将他变为·int tmp = a * 2; const int &c = tmp,这样,c就绑定了一个临时量对象,所谓临时量对象就是编译器需要一个空间来暂存表达式的求职结果时候创建的未命名对象。
  • 同时,当d定义为非常量的时候,就允许修改d,但是修改d的左右时修改临时量的值而非表达式的值,这样的情况下修改d就毫无意义,因此C++将这样的行为归为非法

5.4 对const的引用去引用一个非const对象

int i = 42;
int &a = i;
const int &b = i;
a = 0; //正确
b = 0; //错误,因为b是一个常量引用

如上,用一个常量引用去引用一个非常量对象,作用是使得不能通过修改引用的方式修改对象的值,但是依然可以通过其他的方法,比如直接给i赋值的行为,去修改对象的值

5.5 指针和const

  • 和引用类似,指向常量的指针不能用于改变其所指对象的值,想要存放常量对象的地址,只能使用指向常量的指针
const double PI = 3.14;
double *ptr = Π    //错误,ptr不是常量指针,是普通指针
const double *dptr = Π //正确,dptr是一个常量指针
*dptr = 42;   //错误,不能给*dptr赋值
  • 指针的类型必须与所指对象的类型一直,和引用一样,也有两个例外,一个就是允许常量指针指向非常量对象
  • 指向常量的指针没有规定所指对象是常量,仅仅是要求不能通过该指针改变对象的值,而没有规定那个对象不能通过其他途径改变

所谓常量指针,常量引用,可以理解为指针和引用自以为指向了一个常量,所以自觉地不去改变对象的值

5.6 const 指针

  • 指针是对象,但是引用不是,所以和引用不同的是,允许把指针本身定为一个常量。
  • 常量指针必须初始化,并且一旦初始化完成,他的值(也就是存放在指针里的地址就不能改变了),把*放在const之前说明指针是一个常量,代表不变的是指针本身而非他指向的值
int errNum = 0;
int *const curErr = &errNum;  //一个指向errNum的常量指针
const double PI = 3.14159;
const double *const pip = *pi;
  • 弄清楚声明最行之有效的方式是从右往左,例如对于pip而言,首先可以从const得知这是一个常量对象,然后从*得知这是一个常量指针,然后从const double得知这是一个指向类型为const double的常量指针
  • 常量指针并不意味着不能通过指针修改它指向的值,能否修改完全取决于对象的类型

5.7 顶层const

  • 根据5.5,5.6得知,指针本身是不是常量和指针指向的是不是常量是两个相互独立的问题,用名词顶层表示指针本身是个常量,底层表示指针所指的对象是个常量
  • 更一般的,顶层const可以表示任意的对象是常量,底层const则与指针和引用等复合类型部分有关,指针类型一般既可以是顶层const也可以是底层const
int i = 0;
int *const p1 = &i; //常量指针,不能改变p1的值,顶层const
const int ci = 42; //常量,不能改变ci的值,顶层const
const int *p2 = &ci; //允许改变p2的值,这是一个底层const
const int *const p3 = p2; //一个指向const int的常量指针,靠右的const是底层const,靠左的是顶层const
const int &r = ci; //指向const int的引用,底层const
  • 当执行对象的拷贝操作时,常量是顶层const和底层const的区别明显,其中顶层const不收什么影响
  • 但是底层const的限制却不能忽视,当执行对象拷贝操作时,拷入拷出的对象必须具有相同的底层const资格,或者两个对象的数据类型可以相互转换,非常量可以转化为常量,反之则不行
i = ci;  //拷贝ci的值,ci是一个顶层const,无影响
p2 = p3; //拷贝p3的值,p2和p3指向的对象类型相同,p3顶层const不受影响

int *p = p3;  //错误,p3包含底层const定义,p没有
p2 = p3; //正确,p2,p3都是底层const
p2 = &i; //正确,int*可以转化为const int *
int &r = ci; //错误,r没有底层const
const int &r2 = i; //可以,const int &可以绑定在普通int上

5.8 常量表达式

  • 常量表达式是指值不会改变并且在编译过程中就得到了计算结果的表达式,显然字面值属于常量表达式,用常量表达式初始化的const对象也是常量表达式
  • 一个对象(或者表达式)是不是常量表达式由它的数据类型和初始值共同决定,例如
const int max_files = 20;  //是常量表达式
const int limit = max_files + 1;  //是常量表达式
int staff_size = 27; //不是常量表达式,因为staff_size不是常量
const int sz = getsize(); //不是常量表达式,因为getsize()不是常量

5.9 constexpr 变量

  • 在一个复杂系统中,很难分辨一个初始值到底是不是常量表达式。C++11中规定,允许将变量声明为constexpr类型以便由编译器来验证变量的值是否是一个常量表达式
  • 声明为constexpr的变量一定是一个常量,并且必须使用常量表达式初始化
constexpr int mf = 20; //20是常量表达式
constexpr int limie = mf + 1; //mf + 1是常量表达式
constexpr int sz = size(); //只有当size()是一个constexpr函数时才是一条正确的声明语句
  • 一般来说,如果认定一个变量是常量表达式,就把他声明为constexpr类型
  • 因为常量表达式的值需要在编译时得到计算,因此对声明constexpr时用到的类型必须有所限制,这些类型一般比较简单和显而易见,称为字面值类型
  • 算术类型,引用和指针都属于字面值类型,自定义类,IO库,string类型等不属于字面值类型,因此不能定义为constexpr
  • 尽管指针和引用都能定义为constexpr,但是他们的初始值受到严格限制,一个constexpr指针的初始值必须是nullptr或者0或者是存储于某个固定地址的对象
  • 如果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是一个整型常量
constexpr const int *p = &i; //p是一个指向整型常量的常量指针
constexpr int *p1 = &j;   //p1是一个指向整形的常量指针

6 处理类型

6. 类型别名

//typedef
typedef double wages; //wages是double的同义词、
typedef wages,base, *p; //base是double的同义词,p是double*的同义词

//using
using SI = Sales_item; //SI是Sales_item的同义词

指针、常量和类型别名

typedef char *pstring;
const pstring cstr = 0; 
const pstring *ps;
  • 如上,由于const是对给定类型的修饰,const pstring实际上是一个指向char的常量指针,而不是一个指向常量字符的指针
  • ps指向的对象是一个指向常量类型的指针,const是一个底层const

6.2 auto类型

  • 让编译器代替我们去分析表达式所属的类型,auto让编译器通过初始值来推算变量的类型。
auto item = val1 + val2; 
  • 如上,如果val1和val2是double,则item也是double类型,如果是int,则item也是int
  • 一条声明语句只能有一个基本数据类型,所以语句中所有的变量的初始基本数据都必须一样
auto i = 0, *p = &i;  //正确,i是整数,*p是整型指针
auto sz = 0, pi = 3.14; //错误: sz和pi的类型不一致

· auto一般会忽略掉顶层const,将底层const保留下来

const int ci = i, &cr = ci;//ci是一个常量,const是一个顶层const
//auto->int
auto b = ci; //b是一个int,const被忽略了
auto c = cr; //c是一个整数(cr是ci的别名)
//auto->int*
auto d = &i; //d是一个整形指针(int的地址就是指向int的指针)
//auto->const int*
auto e = &ci; //e是一个指向整数常量的指针(对常量对象取地址则是一种底层const)
  • 如果希望auto可以被推断为一个顶层const,需要明确指出const auto f = ci;
  • 可以将引用的类型设置为auto
//auto->int
auto &g = ci; //g是一个常量引用,绑定到ci
auto &h = 42; //错误:不能为非常量引用绑定字面值
const auto &j = 42; //可以为常量引用绑定字面值
  • 在一条语句中定义多个变量,初始值必须是同一种类型
auto k = ci, &l = i; //auto->int
auto &m = ci, *p = &ci; //auto->const int
auto &n = i, *p2 = &ci; //错误,第一个auto->int,第二个auto->const int

6.3 decltype类型指示符

  • 当我们想利用某一表达式的值推断变量的类型,但是不想用这个表达式给这个变量做初始化的时候,可以考虑采用decltype
  • decltype(f()) sum = x; sum的类型就是f()的返回类型,编译器并不实际调用f(),而是使用当调用发生时f的返回值类型作为sum的类型。换言之,sum的类型就是加入f被调用时候返回的类型
  • declype处理顶层const和引用的方式和auto有些不同
const int ci = 0, &cj = ci;
decltype(ci) x = 0; //decltype->const int
decltype(cj) y = 0; //decltype->const int&
decltype(cj) z;     //decltype->const int&,因为z没有被初始化,所以该语句错误
  • 如图,当cj是一个引用的时候,y也是一个引用。引用从来都作为所指对象的同义词所用,但是在decltype中例外

  • 如果decltype使用的表达式不是一个变量,则返回表达式结果对应的类型。如果返回一个引用类型,意味着该表达式的结果对象能够作为一条赋值语句的左值。

int i = 42, *p = &i, &r = i;
decltype(r + 0) b; //正确,加法的结果是int,和r是个引用无关。因此decltype -> int
decltype(*p) c;    //错误,c是&int,必须初始化
  • 对于decltype所用的表达式来说,如果变量名加上了一对括号,则得到的类型与不加括号时会有不同。如果decltype使用的是一个不加括号的变量,则得到的结果就是该变量的类型;如果加上了一层或多层括号,编译器就会把他当成一个表达式,变量是一种可以作为赋值语句左值的特殊表达式,所以这样decltype就会得到引用类型
decltype((i)) d; //错误: d 是int&,必须初始化
decltype(i) e; //正确: e是一个(未初始化的) int 

6.4 自定义数据结构

  • C++语言允许用户以类的形式自定义数据类型
  • 类体右侧表示结束的花括号后必须写一个分号,因为类体后面可以紧跟变量名表示该类型对象的定义,分号表示声明符的结束,通常来说不要把类的定义和对象的定义放在了一起,这表示把两种不同的实体的定义放在了同一个语句,这种行为不被建议。
struct SD {
  string a;
  double b;
}data;
//等价于
struct SD{
  string a;
  double b;
};
SD data;

6.5 头文件和预处理器

  • 头文件
    • 头文件通常包含那些只能被定义一次的实体,例如类、const和constexpr变量
    • 头文件经常使用到其他头文件的功能,例如Sale_data类包含一个string成员,因此Sale_data.h必须包含string.h头文件。同时使用Sales_data类的程序为了能操作成员也需要再包含一次string.h,这样使用Sales_data的程序就包含了两次string.h头文件
  • 预处理器
    • 确保头文件多次包含仍能够安全工作的常用技术是预处理器,预处理器是编译之前执行的一段程序,可以部分改变我们所写的程序。
    • 之前用到的预处理功能#include,预处理器看到include标记时就会用指定的头文件内容代替#include
  • 头文件保护符
    • 头文件保护符依赖于预处理变量。预处理变量有两种状态:已定义和未定义,#define指令把一个名字设定为预处理变量,另外两个指令则分别检查某个指定的预处理变量是否已经定义
      • /#ifdef当且仅当变量已定义时为真
      • /#ifndef当且仅当变量未定义时为真
      • 一旦结果为真,执行到#endif
//这样可以有效防止重复包含的发生
#ifndef SALES_DATA_H
#define SALES_DATA_H
#include <string>
struct Sales_data {
    std::string a;
    double b;
};
#endif
  • tips: 整个程序中预处理变量包括头文件保护符必须唯一,通常基于头文件中类的名字构建保护符的名字,以确保其唯一性。为了避免和程序中的其他实体发生名字冲突,一般把预处理变量的名字全部大写
  • 头文件即使还没有包含在任何头文件中,也应该设置保护符,没必要太在乎程序需不需要
posted @ 2020-05-11 08:12  Hugh_Locke  阅读(414)  评论(0编辑  收藏  举报