C过渡到C++ - 变量和基本类型(1)

变量和基本类型



2.1 基本内置类型

和c基本一样

在这里插入图片描述
在这里插入图片描述



2.2 变量定义


列表初始化

现在要定义一个名为 “x” 的int变量,并初始化为0,下列4条语句都可以实现

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

作为C++11新标准的一部分,用花括号来初始化变量得到了全面应用,我们暂时先将这种初始化形式称为列表初始化

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

long double ld = 3.1415926536;
int a{ld}, b = {ld};     // 错误:转换为执行,因为存在丢失信息的危险
int c(ld), d = ld;       // 正确:转换执行了,但也确实丢失了信息

默认初始化

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

在函数体内部的内置类型变量将不被初始化,一个未初始化的内置类型变量的值是未定义的。

不管怎么样,定义变量的时候最好还是进行初始化,这是一个良好的编程习惯,比如定义指针时,未做初始化处理,将导致野指针。


声明和定义

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

如果想声明一个变量而非定义它,就在变量名前加上关键字“extern”,而且不要进行显式初始化变量:

extern int i;    // 声明而非定义i
int j;           // 声明并定义j

extern double pi = 3.1416; // 定义 (在函数内部会报错

C++是一种静态类型(statically typed)语言,其含义是在编译阶段检查类型。其中,检查类型的过程称为类型检查(type checking)


标识符

在这里插入图片描述


名字的作用域

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

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

一个典型的示例:

#include <iostream>

int mian()
{
    int sum = 0;
    //sum用于存放从1到10所有数的和
    for (int val = 1; val <= 10; ++val)
    {
        sum += val;
	}
    std::cout << "Sum of 1 to 10 inclusive is " << sum << std::endl;
    
    return 0;
}

这段程序定义了3个名字:miansumval,同时使用了命名控件名字std,该空间提供了2个名字coutcin供程序使用。

mian定义于所有花括号之外,拥有全局作用域。名字sum定义于mian函数所限定的作用域之内,从什么summian函数结束都可以访问,拥有块作用域。名字val定义于for语句内,在for语句内可以访问val,但在mian函数其他地方就不能访问它了。


嵌套的作用域

作用域中一旦声明了某个名字,它所嵌套着的所有作用域中都能访问该名字。同时、允许在内层作用域内重新定义外层作用域已有的名字:

#include <iostream>

/* 该程序仅用于说明:函数内部不宜定义与全局变量同名的新变量 */

int reused = 42;  //reused拥有全局作用域
int mian()
{
	int unique = 0;
    
    std::cout << reused << " " << unique << std::endl;   //输出#1 
    
    int reused = 0;
    
    std::cout << reused << " " << unique << std::endl;    //输出#2 访问局部变量reused
    std::cout << ::reused << " " << unique << std::endl;  //输出#3 访问全局变量reused
    
    return 0;
}
  • 输出#1 出现在局部变量reused定义之前,因此这条语句使用全局作用域中定义的名字reused,输出:42 0
  • 输出#2 发生在局部变量reused定义之后,此时局部变量reused正在作用域内(in scope),因此第二条输出语句使用的是局部变量reused而非全局变量,输出:0 0
  • 输出#3 使用作用域操作符来覆盖默认的作用域规则,因为全局作用域本身并没有名字,所以当作用域操作符的左侧为空时,向全局作用域发出请求获取作用域操作符右侧名字对应的变量。结果是,第三条输出语句使用全局变量reused,输出:42 0



2.3 复合类型

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


2.3.1 引用

分“右值引用”和“左值引用”,一般我们所“引用”多指“左值引用”

引用并非对象,相反的,它只是为一个已经存在的对象所起的另外一个名字。

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

定义了一个引用之后,对其进行的所有操作都是在与之绑定的对象上进行的。

为引用赋值,实际上是把值赋给了与引用绑定的对象。获取引用的值,实际上是获取了与引用绑定的对象的值。同理,以引用作为初始值,实际上是以与引用绑定的对象作为初始值:

//正确: refVa13绑定到了那个与refVa1绑定的对象上,这里就是绑定到ival上
int &refval3 = refVal;

//利用与refVal绑定的对象的值初始化变量i
int i = refval; //正确: i被初始化为ival的值

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


引用的定义

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


2.3.2 指针

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

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

获取对象的地址
int ival = 42;
int *p = &ival; // p存放变量ival的地址,或者说p是指向变量ival的指针 

double dval;
double *pd = &dval; //正确: 初始值是double型对象的地址
double *pd2 = pd;   //正确: 初始值是指向double对象的指针

int *pi = pd;       //错误: 指针pi的类型和pd的类型不匹配
pi = &dval;         //错误: 试图把double型对象的地址赋给int型指针 

指针值

指针的值(即地址)应属下列4种状态之一:

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

空指针

空指针(null pointer)不指向任何对象,在试图使用一个指针之前代码可以首先检查它是否为空。以下列出几个生成空指针的方法:

int *pl = nullptr;  //等价于int *pl = 0;
int *p2 = 0;        //直接将p2初始化为字面常量0
//需要首先#include cstdlib
int *p3 = NULL;     //等价于int *p3 =0;

当用到一个预处理变量时,预处理器会自动地将它替换为实际值,因此用NULL初始化指针和用0初始化指针是一样的。在新标准下,现在的C+程序最好使用nul1ptr,同时尽量避免使用NULL。

建议:初始化所有指针


使用未经初始化的指针是引发运行时错误的一大原因。和其他变量一样,访问未经初始化的指针所引发的后果也是无法预计的。通常这一行为将造成程序崩溃,而且一旦崩溃,要想定位到出错位置将是特别棘手的问题。

在大多数编译器环境下,如果使用了未经初始化的指针,则该指针所占内存空间的当前内容将被看作一个地址值。访问该指针,相当于去访问一个本不存在的位置上的本不存在的对象。糟糕的是,如果指针所占内存空间中恰好有内容,而这些内容又被当作了某个地址,我们就很难分清它到底是合法的还是非法的了。

因此建议初始化所有的指针,并且在可能的情况下,尽量等定义了对象之后再定义指向它的指针。如果实在不清楚指针应该指向何处,就把它初始化为nullptr或者0,这样程序就能检测并知道它没有指向任何具体的对象了。


void* 指针

void*是一种特殊的指针类型,可用于存放任意对象的地址。一个void*指针存放着一个地址,这一点和其他指针类似。不同的是,我们对该地址中到底是个什么类型的对象并不了解:

double obj = 3.14, *pd = &obj;
                  // 正确: void*能存放任意类型对象的地址
void *pv = &obj;  // obj可以是任意类型的对象
pv = pd;          // pv可以存放任意类型的指针

利用void*指针能做的事儿比较有限:拿它和别的指针比较、作为函数的输入或输出,或者赋给另外一个void*指针。不能直接操作void*指针所指的对象,因为我们并不知道这个对象到底是什么类型,也就无法确定能在这个对象上做哪些操作。

既括说来,以void*的视角来看内存空间也就仅仅是内存空间,没办法访问内存空间中所存的对象。


理解复合类型的声明
int *p1, p2;    //P1是指向int的指针, p2是int
int *p1, *p2;   //P1和p2都是指向int的指针

建议将* (或是&)与变量名连在一起。



2.4 const 限定符

const定义的变量,在初始化之后不可被改变


const 和 指针

const int x = 0;
int *p1 = &x;        //错误,p1只是一个普通指针
const int *p2 = &x;  //正确

int y = 0;   
const *p3 = &y;  //正确
*p3 = 2;         //错误,(const *)  (但y可以改变,可以用于权限管理)

int *const p4 = &y;       //正确,p4将一直指向y
const int *const p5 = x;  //正确,p5是一个指向常量对象的常量指针

顶层 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 = 42;    //不能改变ci的值, 这是一个顶层const
const int *p2 = &ci;  //允许改变p2的值, 这是一个底层const
const int *const p3 = p2;  //靠右的const是顶层const, 靠左的是底层const
const int &r = ci;         //用于声明引用的const都是底层const

/* 使用示例 */
i = ci;    //正确: 拷贝ci的值, ci是一个顶层const,对此操作无影响
p2 = p3;   //正确: p2和p3指向的对象类型相同, p3顶层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上

constexpr 变量

常量表达式(const expression) ,声明为此类型表示此变量一定为常量,而且必须用常量初始化



2.5 处理类型


类型别名

有两种方法可用于定义类型别名。传统的方法是使用关键字typedef

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

关键字typedef作为声明语句中的基本数据类型的一部分出现,含有typedef的声明语句定义的不再是变量而是类型别名。

新标准规定了一种新的方法,使用 别名声明(alias declaration) 来定义类型的别名:

using sI = Sales item;  // S1是Sales item的同义词

这种方法用关键字using作为别名声明的开始,其后紧跟别名和等号,其作用是把等号左侧的名字规定成等号右侧类型的别名。类型别名和类型的名字等价,只要是类型的名字能出现的地方,就能使用类型别名


auto类型说明符

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

const int ci = i, &cr = ci;
auto b = ci;   //b是一个整数(ci的顶层const特性被忽略掉了)
auto c = cr;   //c是一个整数(cr是ci的别名, ci本身是一个顶层const )
auto d = &i;   //d是一个整型指针(整數的地址就是指向整數的指针)
auto e = &ci;  //e是一个指向整数常量的指针(对常量对象取地址是一种底层const)

decltype类型指示符

选择并返回操作符的数据类型。只得到类型,不实际计算表达式的值。

decltype (f()) sum = x;   // sum的类型就是函数f的返回类型 



2.6 自定义数据类型


(1) 类

数据结构是把一组相关的数据元素组织起来,然后使用它们的策略和方法。

类一般不定义在函数体内,为了确保各个文件中类的定义一致,类通常被定义在头文件中,而且类所在头文件的名字应该与类的名字一样。

头文件通常包含那些被定义一次的实体。


(2) 预处理器

#ifndef __SALES_DATA_H__ 
#define __SALES_DATA_H__ 

#endif  /* __SALES_DATA_H__ */

一般把预处理变量的名字全部大写。



小结

类型是C++编程的基础。

类型规定了其对象的存储要求和所能执行的操作。C++语言提供了一套基础内置类型,如int和char等,这些类型与实现它们的机器硬件密切相关。类型分为非常量和常量,一个常量对象必须初始化,而且一旦初始化其值就不能再改变。此外,还可以定义复合类型,如指针和引用等。复合类型的定义以其他类型为基础。

C++语言允许用户以类的形式自定义类型。C++库通过类提供了一套高级抽象类型,如输入输出和string等。

  • 3
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

nepqiu

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值