附:《C++ Primer》的第二章的指针部分,主要是指针的基础,可以很好地区分与引用的区别。关于指针与数组在后面的章节还会有详解。后续结合王慧、王浩的《零基础学C++》理解的更深刻一些,害,可能还是我太菜了。
基本形式:类型说明符 变量名1,变量名2,…;
当对象在创建时获得了一个特定的值,我们说这个对象被初始化(initialized)了。
术语:何为对象?
C++程序员们在很多场合都会使用对象(object)这个名词。通常情况下,对象是指一块能存储数据并具有某种类型的内存空间。
一些人仅在与类有关的场景下才使用“对象”这个词。另一些人则已把命名的对象和未命名的对象区分开来,他们把命名了的对象叫做变量。还有一些人把对象和值区分开来,其中对象指能被程序修改的数据,而值(value)指只读的数据。
《C++ Primer》书遵循大多数人的习惯用法,即认为对象是具有某种数据类型的内存空间。在使用对象这个词时,并不严格区分是类还是内置类型,也不区分是否命名或是否只读。
列表初始化
C++语言定义了初始化的好几种不同形式,例如,要想定义一一个名为units_ sold
的int
变量并初始化为0,以下的4条语句都可以做到这一点:
int units_sold = 0;
int units_sold = {0};
int units_sold{0};
int units_sold(0);
作为C++11新标准的一部分, 用花括号来初始化变量得到了全面应用,而在此之前,这种初始化的形式仅在某些受限的场合下才能使用。出于3.3.1节(第88页)将要介绍的原因,这种初始化的形式被称为列表初始化(list initialization)。无论是初始化对象还是某些时候为对象赋新值,都可以使用这样一组由花括号括起来的初始值。
默认初始化
定义于函数体内的内置类型的对象如果没有初始化,则其值未定义。类的对象如果没有显式地初始化,则其值由类确定。
解释下列定义的含义,对于非法的定义,请说明错在何处并将其改正。
(a) std::cin >> int input_value;
(b) int i = { 3.14 };
© double salary = wage = 9999.99;
(d) int i = 3.14;
解答: (a): 应该先定义再使用。
int input_value = 0;
std::cin >> input_value;
(b): 用列表初始化内置类型的变量时,如果存在丢失信息的风险,则编译器将报错。
double i = { 3.14 };
©: wage 是未定义的,应该在此之前将其定义。
double wage;
double salary = wage = 9999.99;
(d): 不报错,但是3.14是浮点型,而定义整型int,小数部分会被截断。输出 i=3; 可以改成
double i = 3.14;
下列变量的初值分别是什么?
std::string global_str;
int global_int;
int main()
{
int local_int;
std::string local_str;
}
global_int
是全局变量,所以初值为 0 。 local_int
是局部变量并且没有初始化,它的初值是未定义的。 global_str
和 local_str
是 string 类的对象,该对象定义了默认的初始化方式,即初始化为空字符串。
声明(delaration)使得名字为程序所知,一个文件如果想使用别处定义的名字则必须包含对那个名字的声明。而定义(definition)负贵创建与名字关联的实体。
变量声明规定了变量的类型和名字,在这一点上定义与之相同。但是除此之外,定义还申请存储空间,也可能会为变量赋一个初始值。
如果想声明一个变量而非定义它,就在变量名前添加关键字extern
,而且不要显式地初始化变量: .
extern int 1; //声明立而非定义i
int j; //声明并定义了
任何包含了显式初始化的声明即成为定义。我们能给由extern
关键字标记的变量赋一个初始值,但是这么做也抵消了extern
的作用。extern
语旬如果包含初始值就不再是声明,而变成定义了:
extern double pi - 3.1416; //定义
在函数体内部,如果试图初始化一 个由extern
关键字标记的变量,将引发错误。
变量能且只能被定义一次,但是可以被多次声明。
如果要在多个文件中使用同一个变量,就必须将声明和定义分离。此时,变量的定义必须出现在且只能出现在一个文件中,而其他用到该变量的文件必须对其进行声明,却绝对不能重复定义。
指出下面的语句是声明还是定义:
(a) extern int ix = 1024; 定义
(b) int iy; 声明并定义
© extern int iz; 声明
关键概念:静态类型
C++是一种静态类型( statically typed)语言,其含义是在编译阶段检查类型。其中,检查类型的过程称为类型检查( type checking )。我们已经知道,对象的类型决定了对象所能参与的运算。在C++语言中,编译器负责检查数据类型是否支持要执行的运算,如果试图执行类型不支持的运算,编译器将报错并且不会生成可执行文件。
程序越复杂,静态类型检查越有助于发现问题。然而,前提是编译器必须知道每一个实体对象的类型,这就要求我们在使用某个变量之前必须声明其类型。
C++的标识符(identifier) 由字母、数字和下画线组成,其中必须以字母或下画线开头。标识符的长度没有限制,但是对大小写字母敏感。
如表2.3和表2.4所示,C++语言保留了一些名字供语言本身使用,这些名字不能被用作标识符。
同时,C++也为标准库保留了一些名字。 用户自定义的标识符中不能连续出现两个下画线,也不能以下画线紧连大写字母开头。此外,定义在函数体外的标识符不能以下画线开头。
变量命名规范
●标识符要能体现实际含义,能有效提高程序的可读性
●变量名一般用小写字母,如index, 不要使用Index或INDEX。
●用户自定义的类名一般以大写字母开头,如sales_ item.
●如果标识符由多个单词组成,则单词间应有明显区分,如student_ loan或studentLoan。
请指出下面的名字中哪些是非法的?
作用域(scope)是程序的一部分, 在其中名字有其特定的含义。C++语言中大多数作用域都以花括号分隔。
同一个名字在不同的作用域中可能指向不同的实体。名字的有效区域始于名字的声明语句,以声明语句所在的作用域末端为结束。
嵌套的作用域
作用域能彼此包含,被包含(或者说被嵌套)的作用域称为内层作用域(inner scope),包含着别的作用域的作用域称为外层作用域(outer scope)。
作用域中一旦声明了某个名字,它所嵌套着的所有作用域中都能访问该名字。同时,允许在内层作用域中重新定义外层作用域已有的名字。此时,重新定义的局部变量会覆盖全局变量。如果显式地访问全局变量,就使用作用域操作符::。
下面程序中 j 的值是多少?
int i = 42;
int main()
{
int i = 100;
int j = i;
}
j
的值是 100
,局部变量 i
覆盖了全局变量 i
。
下面的程序合法吗?如果合法,它将输出什么?
int i = 100, sum = 0;
for (int i = 0; i != 10; ++i)
sum += i;
std::cout << i << " " << sum << std::endl;
合法。输出是 100 45
。
一条声明语句由一个基本数据类型(base type)和紧随其后的一个声明符(declarator)列表组成。每个声明符命名了一个变量并指定该变量为与基本数据类型有关的某种类型。
目前为止,我们所接触的声明语句中,声明符其实就是变量名,此时变量的类型也就是声明的基本数据类型。其实还可能有更复杂的声明符,它基于基本数据类型得到更复杂的类型,并把它指定给变量。
引用(reference) 为对象起了另外一个名字(可以理解为别名),引用类型引用(refers to) 另外一种类型。
引用必须被初始化,且引用的初始值必须是一个对象;
通过将声明符写成&d
的形式来定义引用类型,其中d
是声明的变量名:
int ival = 1024;
int &refVal =ival; // refVal指向ival (是ival的另一个名字)
int &refVal2; //报错:引用必须被初始化
**一般情况下,引用的类型都要和与之绑定的对象严格匹配。**而且,引用只能绑定在对象上,而不能与字面值或某个表达式的计算结果绑定在一起:
int &refVal4 =10; // 错误:引用类型的初始值必须是一个对象
double dval =3.14;
int &refVa15 = dval; //错误:此处引用类型的初始值必须是int型对象
下面的哪个定义是不合法的?为什么?
int ival = 1.01;
int &rval1 = 1.01; //引用必须绑定在对象上
int &rval2 = ival;
int &rval3; //引用必须初始化。
考察下面的所有赋值然后回答:哪些赋值是不合法的?为什么?哪些赋值是合法的?它们执行了哪些操作?
int i = 0, &r1 = i; double d = 0, &r2 = d;
(a) r2 = 3.14159; 合法。给 d 赋值为 3.14159。
(b) r2 = r1; 合法。会进行自动类型转换(int->double)
(c) i = r2; 合法。会发生小数截取,取整
(d) r1 = d; 合法。会发生小数截取,取整
int i, &ri = i;
i = 5; ri = 10;
std::cout << i << " " << ri << std::endl;
输出: 10 10
指针实现了对其他对象的间接访问。
与引用相比,不同点。
其一,指针本身就是一个对象,允许对指针赋值和拷贝,而且在指针的生命周期内它可以先后指向几个不
同的对象。其二,指针无须在定义时赋初值。和其他内置类型一样,在块作用域内定义的指针如果没有被初始化,也将拥有一个不确定的值。
定义指针类型的方法将声明符写成*d
的形式,其中d
是变量名。如果在一条语句中定义了几个指针变量,每个变量前面都必须有符号:*
。
指针存放某个对象的地址,要想获取该地址,需要使用取地址符〈操作符&):
int ival = 42;
int *p = &ival; //p存放变量ival的地址,或者说p是指向变量ival的指针
第二条语句把p定义为一个指向int 的指针,随后初始化p令其指向名为ival
的int
对象。因为引用不是对象,没有实际地址,所以不能定义指向引用的指针。
指针的类型都要和它所指向的对象严格匹配:
double dval;
double *pd=&dval; //正确:初始值是double型对象的地址
double *pd2= pd; //正确:初始值是指向double对象的指针
int *pi = pd; //错误:指针pi的类型和pd的类型不匹配
pi =&dval; //错误:试图把double型对象的地址赋给int型指针
因为在声明语句中指针的类型实际上被用于指定它所指向对象的类型,所以二者必须匹配。如果指针指向了一个其他类型的对象,对该对象的操作将发生错误。
指针的值(即地址)应属下列4种状态之一:
1.指向一个对象。
2.指向紧邻对象所占空间的下一个位置。 (迭代器)
3.空指针,意味着指针没有指向任何对象。(NULL 或者nullptr)
4.无效指针,也就是上述情况之外的其他值。
如果指针指向了一个对象,则允许使用**解引用符( 操作符*)**来访问该对象:
int ival = 42;
int *p = &ival; // p存放着变量ival的地址,或者说p是指向变量ival的指针
cout << *p; //由符号*得到指针p所指的对象,输出42
对指针解引用会得出所指的对象,因此如果给解引用的结果赋值,实际上也就是给指针所
指的对象赋值:
*p=0; //由符号*得到指针p所指的对象,即可经由p为变量ival赋值
cout << *p; //输出0
如上述程序所示,为*p
赋值实际上是为p
所指的对象赋值。
解引用操作仅适用于那些确实指向了某个对象的有效指针。
关键概念:某些符号有多重含义
像&
和*
这样的符号,既能用作表达式里的运算符,也能作为声明的一部分出现,符号的上下文决定了符号的意义:
int i=42;
int &r=i; // &紧随类型名出现,因此是声明的一部分,&是一个引用
int *p; // *紧随类型名出現,因此是声明的一部分,P是一个指针
P= &i; //&出现在表达式中,是一个取地址符
*p=i ; //*出现在表达式中,是一个解引用符
int &r2 =*p: //&后是声明的一部分,*是一个解引用符
在声明语句中,&
和*
用于组成复合类型;在表达式中,它们的角色又转变成运算符。在不同场景下出现的虽然是同一个符号,但是由于含义截然不同,所以完全可以把它当作不同的符号来看待。
空指针(null pointer) 不指向任何对象,在试图使用一个指针之前代码可以首先检查
它是否为空。以下列出几个生成空指针的方法:
int *p1 = nu11ptr; //等价于int*p1=0;
int *p2 =0; //直接将p2初始化为字面常量0
int *p3 = NULL; //NULL在#include中定义,等价于int *p3=0;
得到空指针最直接的办法就是用字面值nullptr
来初始化指针,这也是C++11新标准刚刚引入的一-种方法。nullptr
是一种特殊类型的字面值,它可以被转换成任意其他的指针类型。
注意:
1、把int变量直接赋给指针是错误的操作,即使int变量的值恰好等于0也不行。
int zero = 0;
int *pi = zero; //错误:不能把int变量直接赋给指针
中文版49此处有误(pi = zero),修改如上。
2、初始化所有指针
使用未经初始化的指针是引发运行时错误的一大原因。
和其他变量一样,访问未经初始化的指针所引发的后果也是无法预计的。通常这一行为将造成程序崩溃,而且一旦崩溃,要想定位到出错位置将是特别棘手的问题。
在大多数编译器环境下,如果使用了未经初始化的指针,则该指针所占内存空间的当前内容将被看作一个地址值。访问该指针,相当于去访问一个本不存在的位置上的本不存在的对象。糟糕的是,如果指针所占内存空间中恰好有内容,而这些内容又被当作了某个地址,就很难分清它到底是合法的还是非法的了。
3、区分赋值和指针
给指针赋值就是令它存放一个新的地址,从而指向一个新的对象。
int i=42;
int *pi = 0; //pi被初始化,但没有指向任何对象
int *pi2 = &i; //pi2被初始化,存有i的地址
int *pi3; //如果pi3定义于块内,则pi3的值是无法确定的
pi3 = pi2; // pi3和pi2指向同一个对象i
pi2 = 0; //现在pi2不指向任何对象了
区分一条赋值语句,到底是改变了指针的值,还是改变了指针所指对象的值
赋值永远改变的是等号左侧的对象。
pi = &ival; // pi的值被改变,现在pi指向了ival
意思是为pi
赋一个新的值,也就是改变了那个存放在pi
内的地址值。相反的,如果写
出如下语句, .
*pi = 0; // ival 的值被改变,指针pi并没有改变
则*pi
(也就是指针pi
指向的那个对象)发生改变。
void*
是一种特殊的指针类型,可用于存放任意对象的地址。一个void*
指针存放着一个地址,这一点和其他指针类似。不同的是,我们对该地址中到底是个什么类型的对象并不了解:
double obj = 3.14, *pd = &obj; //正确: void*能存放任意类型对象的地址
void *pv =obj; // obj可以是任意类型的对象
pv=pd; // pv可以存放任意类型的指针
利用void*
指针能做的事儿:拿它和别的指针比较、作为函数的输入或输出,或者赋给另外一个void指针。不能直接操作void*
指针所指的对象,因为我们并不知道这个对象到底是什么类型,也就无法确定能在这个对象上做哪些操作。概括说来,以void*的视角来看内存空间也就仅仅是内存空间,没办法访问内存空间中所存的对象。
编写代码分别改变指针的值以及指针所指对象的值。
int a = 0, b = 1;
int *p = &a, *q = p;
p = &b; // 改变指针的值
*q = b; //改变指针所指对象的值
说明指针和引用的主要区别
请叙述下面这段代码的作用。
int i = 42;
int *p1 = &i;
*p1 = *p1 * *p1;
让指针 pi
指向 i
,然后将 i
的值重新赋值为 42 * 42 (1764)。
请解释下述定义。在这些定义中有非法的吗?如果有,为什么?
int i = 0;
(a) double* dp = &i; //非法。不能将一个指向 `double` 的指针指向 `int` 。
(b) int *ip = i; // 非法。不能将 `int` 变量直接赋给指针
(c) int *p = &i; //合法。
假设 p 是一个 int 型指针,请说明下述代码的含义。
if (p) // ...判断 p 是不是一个空指针
if (*p) // ...判断 p 所指向的对象的值是不是为 0
给定指针 p,你能知道它是否指向了一个合法的对象吗?如果能,叙述判断的思路;如果不能,也请说明原因。
不能,要先确定这个指针是否合法的,才能判断它所指向的对象是否合法的。
在下面这段代码中为什么 p 合法而 lp 非法?
int i = 42;
void *p = &i;
long *lp = &i;
因为void *
是一种特殊的指针类型,可用于存放任意对象的地址,即可以指向任何类型的对象。而其他指针类型必须要与所指对象严格匹配。
一般来说,声明符中修饰符的个数并没有限制。当有多个修饰符连写在一起时,按照其逻辑关系详加解释即可。以指针为例,指针是内存中的对象,像其他对象一样也有自己的地址,因此允许把指针的地址再存放到另-一个指针当中。
通过*的个数可以区分指针的级别。
int ival = 1024;
int *pi = &ival; // pi 指向一个int型数的指针
int **ppi=π //ppi指向一个int型指针的指针
解引用int型指针会得到一一个int型的数*p
,同样,解引用指向指针的指针会得到一个指针。此时为了访问最原始的那个对象,需要对指针的指针做两次解引用**p
。
引用本身不是一个对象,因此不能定义指向引用的指针。但指针是对象,所以存在对指针的引用:
int i= 42;
int *p; // p是一个int型指针
int *&r = p; // r是一个对指针p的引用
r=&i; //r引用了一个指针,因此给r賦值&i就是令p指向i
*r=0; //解引用上得到i,也就是p指向的对象,将i的值改为0
要理解r
的类型到底是什么,最简单的办法是从右向左阅读r
的定义。离变量名最近的符号(此例中是&r
的符号&
)对变量的类型有最直接的影响,因此r
是一个引用。声明符的其余部分用以确定r
引用的类型是什么,此例中的符号*
说明r
引用的是一个指针。最后,声明的基本数据类型部分指出r
引用的是一-个int
指针。
说明下列变量的类型和值。
int* ip, i, &r = i; //ip 是一个指向 int 的指针, i 是一个 int, r 是 i 的引用。
int i, *ip = 0; // i 是 int , ip 是一个空指针。
int* ip, ip2; // ip 是一个指向 int 的指针, ip2 是一个int。