C++ Primer 学习(第二章)

1.如果表达式里既有带符号类型又有无符号类型,当带符号类型的数为负值时会出现异常的结果,这是因为带符号数会自动地转化为无符号数。例如以下程序:

unsigned u=10;
int u2=42;
std::cou<<u-u2<<std::endl;

输出结果为4294967264。

另外,对于两个无符号数,如果从无符号数中减去一个值时,不论这个值是否为无符号数,我们都必须保证结果不能为一个负值

例如:

unsigned u1=42,u2=10;
std::cout<<u1-u2<<std::endl;
std::cout<<u2-u1<<std::endl;

对于上述代码,第一个输出是正确的,第二个值肯定不是-32。

2.对象是指一块能存储数据具有某种类型内存空间

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

4.一般来说,类型float和double分别有7个和16个有效位,有效位是指整数部分与小数部分加起来的位数,其中不包括小数点。例如,float a=45.345678,则最后一位小数8是没有意义的。

5.char类型的数据在有些机器上默认为是有符号的,在有些机器上默认是无符号的,所以如果使用char进行运算容易出现问题。如果需要使用一个不大的整数时,建议明确指出时unsigned char 还是signed char。

6.取模运算:

表达式结果解释
7 mod 431<2      取商为1    ans=7-4*1=3
-7 mod 41-2<-1   取商为-2   ans=-7-4*(-2)=1
7 mod -4-1-2<-1   取商为-2   ans=7-(-4)*(-2)=-1
-7 mod -4-31<2      取商为1    ans=-7-(-4)*1=-3

规则:以第二个表达式为例,认为-7 / 4的商有两个可能值-1,-2,在两个可能值当中永远取较小的那一个为商,然后用被除数-商*除数即为结果。

7.因为全局作用域本身并没有名字,所以当作用域操作符的左侧为空时,向全局作用域发出请求获取作用域操作符右侧名字对应的变量。

例如:

#include <iostream>
int a = 10;
int main()
{
	int a = 5;
	std::cout << ::a << std::endl;
	return 0;
}

程序的输出结果为10。因为外层的变量a是全局作用域的,内层的变量a是局部作用域的。当作用域操作符的左侧为空时,向全局作用域发出请求获取作用域操作符右侧名字对应的变量,即a。

8.引用并非对象,引用不开辟新的内存空间。相反的,它只是为一个已经存在的对象所起的另一个名字。而指针则是一种对象,它会另外开辟一个内存空间,用以存放某个对象的地址。

9.因为引用不是对象,没有实际地址,故不能定义指向引用的指针

10.将指针定义为空指针有三种方法:

int *p1=nullptr;
int *p2=0;
int *p3=NULL;

但是,不能把int变量直接赋给指针,即使int变量的值恰好等于0也不行。例如:

int zero=0;
int *p=zero;

上述代码虽然zero的值为0,但是由于zero是变量,故这种操作是错误的。

11.假设p是一个指针,则

if(p)

如果p为空指针,则结果为false,只要p不为空指针,结果就为true。

if(*p)

如果指针p指向的对象为0,则结果为false,否则就是true。

12.void* 指针是一种特殊类型的指针,其可用于存放任意对象的地址,不需要与它所指的对象的类型严格匹配。但是也正应如此,void*指针能做的事非常有限:拿它和别的指针比较、作为函数的输入或输出,或者赋给另外一个void*指针。不能直接操作void*指针所指的对象,因为我们并不知道这个对象到底是什么类型,也就无法确定能在这个对象上做哪些操作。

13.对于int *p或者int &p,我们不要迷惑于基本数据类型和类型修饰符的关系,(*或&)不过是声明符的一部分罢了。对于int* p,基本数据类型是int而非int*。*仅仅是修饰了p而已。

14.int *p和int* p没有任何区别,都定义了一个指向int类型对象的指针,但是建议写成前者的形式。

15.通过*的个数可以区分指针的级别。也就是说,**表示指向指针的指针,***表示指向指针的指针的指针。例如:

#include <iostream>
int main()
{
	int ival = 1024;
	int *pi = &ival;//pi指向一个int型的数
	int **ppi = &pi;//ppi指向一个int型的指针
	std::cout << "The value of ival\n"
		<< "direct value: " << ival << "\n"
		<< "indirect value: " << *pi << "\n"//*pi等价于变量ival
		<< "doubly indirect value: " << **ppi << std::endl;//**ppi等价于变量ival
	return 0;
}

执行结果:

  下图表示了ival  pi ppi的关系:

           ppi            pi            ival
                        ---->                    ---->     1024

在定义int *p =&ival时,*是一个声明符,而在后面的*pi中的*表示解引用符,因为pi-->ival,只有一个指针箭头,故只需一个解引用符,*pi就等于ival等于1024;因为ppi-->pi-->ival,有两个箭头,故需要两个解引用符,**ppi就等于ival=1024。所以以后在定义之后再次遇到指针,如果定义的是指针的指针,则遇到两个解引用符表示最开始的值ival。

16.指向指针的引用:

引用本身不是一个对象,因此不能定义指向引用的指针。但指针是对象,故可以定义指向指针的引用。

int i=42;
int *p;
int *&r=p;//r是一个对指针p的引用
r=&i;//r引用了一个指针,因此给r赋值&i,相当于另p指向i  <==>p=&i;
*r=0;//解引用r得到i,也就是p指向的对象,将i的值改为0 <==>*p=0<==>i=0

对于理解r的类型到底是什么,最简单的办法是从右向左阅读r的定义离变量名最近的符号(此例中是&r中的符号&)对变量的类型有最直接的影响,因此r是一个引用。声明符的其余部分用以确定r引用的类型到底是什么,此例中的符号*说明r引用的是一个指针。最后,声明的基本数据类型指出r引用的是一个int指针。

17.引用类型的初始值必须是一个对象,所以下述代码是错误的:

int &p=0;

因为0是一个字面值,不是一个对象(10不占用系统的内存空间),所以上述代码是错误的。

但是初始化常量引用时允许使用任意表达式作为初始值,包括允许为一个常量引用绑定非常量的对象字面值、甚至是一个一般表达式例如下述代码均是正确的:

int i=42;
const int &r1=i;//允许为常量引用绑定非常量的对象
const int &r2=42;//允许为常量引用绑定字面值
const int &r3=r1*2;//允许为常量引用绑定一般表达式

在正常情况下,引用的类型必须与引用对象的类型保持一致,但有两种特例,上述情况是其中的一种特例。另外,对于下面的代码也是合法的:

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

从上述代码可以看到,ri引用了一个int型的数,但是dval却是一个双精度浮点数,因此为了保证让ri绑定一个整数,编译器会把上述代码变成下述形式:

const int temp=dval;//由双精度浮点数生成一个临时的整形常量
const int &ri=temp;//让ri绑定这个临时量

所谓的临时量对象就是当编译器需要一个空间暂存表达式的求值结果时临时创建一个未命名的对象。

至于如果ri不是常量,为什么程序就不合法呢?不合法代码如下:

double dval=3.14;
int &ri=dval;

上述代码的不合法性在于,因为ri不是一个常量引用,所以就允许为ri赋值,这样就允许改变ri所引用对象的值。但是注意,此时绑定的对象是临时量而非dval。程序员既然想让ri引用dval,就是想通过ri改变dval的值,要不然也没有必要去定义ri这个引用,所以C++直接认为这种行为是不合法的。

那么为什么加了const就合法了呢?

这是因为加了const之后,ri就变为一个常量,所以就不允许对ri进行赋值,这样也就不会改变ri所引用对象的值了,故引用类型就允许与引用对象的类型不一致。

总结来说,加const允许引用类型与引用对象类型不一致的根本原因在于const常量不会改变引用对象的值,所以两种类型一不一致就已经无所谓了。

18.包含const的几种指针:(注意区分)

(1)指向常量的指针

const double pi=3.14;
const double *cptr=&pi;

指向常量的指针不能用于改变其所指对象的值。

(2)常量指针

int errNumb=0;
int *const curErr=&errNumb;

常量指针必须初始化,而且一旦初始化完成,它的值(也就是存放在指针中的那个地址)就不能再改变了。 

(3)指向常量对象的常量指针

const double pi=3.14159;
const double *const pip=&pi;

*const表示常量指针(地址不变),const  <类型>  *表示指向常量的指针(对象不能改变)。

19.指针的类型必须与其所指的对象类型一致,但是有两种特例,其中一种特例就是指向常量的指针可以指向非常量对象

例如:

const pi=3.14;
const double *cptr=&pi;
double dval=3.14159;
cptr=&dval;

上述代码中,定义的cptr指针是一个指向常量的指针,但是后来cptr指向了一个非常量对象dval,这段代码是合法的。和常量引用一样,指向常量的指针也没有规定其所指的对象必须是一个常量。所谓指向常量的指针,仅仅要求不能通过该指针改变对象的值,而没有规定那个对象不能通过其他途径改变。 

总结来说:无论是指向常量的指针还是指向常量的引用,不过是指针和引用“自以为是”罢了,它们觉得自己指向了常量,所以自觉不去改变所指对象的值。

再来总结一下引用和指针的特例:

引用:初始化常量引用时允许使用任意表达式作为初始值,包括允许为一个常量引用绑定非常量的对象字面值、甚至是一个一般表达式

指针:允许指向常量的指针指向非常量对象。

20.如下代码:

int i=4;//合法
int &r1=i;//合法
const int &r2=i;//合法
r1=0;//合法
r2=0;//不合法

对const对象的引用可能引用一个并非const的对象,上述代码中,由于r2是一个常量引用,虽然r2绑定(非常量)整数i的行为是合法的,但是不能通过r2去修改i的值,因为常量引用对引用可参与的操作做出了限定。但是这并不意味这不允许通过其他途径改变i的值,在上述代码中,可以通过非常量引用r1去修改i的值,也可以直接i=0去修改i的值。

21.指针本身是一个常量并不意味着不能通过指针去修改所指对象的值,常量指针只是表示指针当中保存的地址是不变的,还是可以去修改其所指对象的值的,能否修改最终还是取决于所指对象的类型,若所指对象是const类型,则不能修改。因此,常量指针既可以指向常量也可以指向非常量。

22.顶层const:指的是本身不可以被改变。

     底层const:指的是自身所指对象不可以被改变。

在我看来,顶层const和底层const的概念仅仅针对指针。顶层const指针意味着指针指向某一个对象之后,指针不能被修改以指向其他对象,即常量指针;底层const指针意味着不同通过解引用的方式来修改该指针指向的对象,比如“*p = ..”是不被底层const所允许的,即指向常量的指针。引用没有顶层const相关概念。普通引用一经绑定,就不能更改绑定对象。凡是声明引用的const都是底层const,即引用所绑定的对象的值不能改变

23.代码:

typedef char *pstring;
const pstring cstr=0;

注意:const是对给定类型的修饰,typedef char *pstring这句话,其实已经说明基本数据类型是指针,即这句话表明pstring是指向char的指针,由于const是对给定类型的修饰,即const是用来修饰指针的,所以它是const指针。

而代码:

const char *cstr=0;

上述代码中,数据类型就变为了char,所以const是用来修饰char的,所以它就是一个指向const char的指针。

所以,当const与typedef一起出现时,不能简单的用字符串进行替换。在本例中,const给予了整个指针以常量属性。

24.常量表达式和constexpr

常量表达式是指值不会改变并且在编译过程就能得到计算结果的表达式。

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

const int a=1;//a是常量表达式
const int b=1+a;//b是常量表达式
int c=27;//c不是常量表达式
const int d=get_size();//d不是常量表达式

上述代码中,尽管c是一个字面值常量,但它的数据类型是int而不是const int,所以不属于常量表达式。另一方面,尽管d是一个常量,但是它的具体值在运行时才能够知道(编译的时候无法得知),故而其不是常量表达式。

constexpr类型:声明为constexpr的变量一定是一个常量,而且必须用常量表达式去初始化。如果我们认定一个变量为常量表达式,那么可以将其声明成constexpr类型。

const 和 constexpr 变量之间的主要区别在于:const 变量的初始化可以延迟到运行时,而 constexpr 变量必须在编译时进行初始化。所有 constexpr 变量均为常量,因此必须使用常量表达式初始化

25.指针和constexpr

注意:一个constexpr指针的初始值必须是nullptr或者0,或者是存储于某个固定地址中的对象。一般来说,函数体内定义的变量的地址是会变化的,因此constexpr指针不能指向这样的变量。但是位于所有函数体之外的对象其地址固定不变,故可以用来初始化constexpr指针。

另外需要注意的是,在constexpr声明中定义的指针,限定符仅对指针有效,而对指针指向的对象无效,所以用constexpr定义的指针是一个常量指针,因为constexpr会把它所定义的对象置成顶层const,即其本身不可变。所以注意理解下面的代码与注释。

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

另外,假设下述代码中的i和j定义于所有函数体之外,注意理解下述代码:

int j=0;
constexpr int i=42;//i和j必须定义于所有函数体之外,这是为了下面常量指针可以指向它们
constexpr const int *p=&i;//p是一个常量指针,指向整型常量i
constexpr int *p1=&j;//p1是一个常量指针,指向整数j

上述代码第三行constexpr修饰常量指针,const修饰整型常量。

26.auto类型说明符

作用:有时候声明变量的时候并不清楚表达式的类型,这时候用auto类型说明符可以让编译器替我们分析表达式所属的类型

由于要根据表达式去分析它的类型,因此用auto定义的变量必须有初始值。

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

auto i=0,*p=&i;//正确,i是整数、p是整型指针
auto sz=0,pi=3.14;//错误,sz和pi的类型不一致

上述第2行代码中,因为0是int型,3.14是double型的,不一致,故第2行代码是不合法的。 

27.auto类型说明符一般会忽略掉顶层const,同时底层const则会保留下来。如果希望推断出的auto类型是一个顶层const,则需要明确指出,即需要在前面手动添加const,但是对于类型为auto的引用,初始值中的顶层属性仍然是会保留下来的。请看下述代码:

int i=0;
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)
const auto f=ci;//ci的顶层const忽略掉了,如果需要顶层const,需手动添加
auto &g=ci;//g是一个整型常量引用,绑定到ci
auto &h=42;//错误:不能为非常量绑定字面值
const auto &j=42;//正确:可以为常量引用绑定字面值
auto &n=i,*p=&ci;//错误:i的类型是int,而&ci的类型是const int

 上述代码第3行:ci本身具有顶层const属性,即ci本身不可变,而由auto类型说明符会忽略顶层const的特性,知b是一个整数,而不是一个整型常量。

第6行:&ci是对常量对象ci取地址,因为该地址是可变的,即e指针本身是可变的,只是e所指的对象不可变,故对常量对象取地址是一种底层const,根据auto类型说明符会保留底层const的特性,知e是一个指向整型常量的指针。另外,需要注意的是,在这里,auto e=&ci;与auto *e=&ci;没有任何区别,都是定义指针e。在此理解之上,最后一行的错误也就好理解了。

28.decltype类型说明符

在有些时候,我们希望从表达式的类型推断出要定义的变量的类型,也就是上面auto类型说明符的功能,但是auto类型说明符有一个缺点就是我们需要用表达式的值去初始化变量,而有时候我们只是想知道定义变量的类型,而不想用表达式的值去初始化变量,这个时候decltype类型说明符就发挥作用了。它的作用是选择并返回操作数的数据类型。如下代码:

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

另外需要注意的是,decltype处理顶层const和引用的方式与auto有所不同。

如果decltype使用的表达式是一个变量,则decltype返回该变量的类型(包括顶层const和引用),而显然auto是忽略顶层const和引用的。例如:

const int ci=0,&cj=ci;
decltype(ci) x=0;//x的类型和ci一样,const int
decltype(cj) y=x;//y的类型和cj一样,const int&,相当于const int &y=x;
decltype(cj) z;//错误,相当于const int &z,z是一个引用,必须初始化

需要注意的是,引用从来都是作为所指对象的同义词出现,只有在decltype处是一个例外。

还有一点也需要注意,如果decltype使用的是表达式而不是一个变量,则decltype返回表达式结果对应的类型。以下代码结合注释好好体会:

int i=42,*p=&i,&r=i;
decltype(r+0) b;//正确:加法的结果是int,因此b是一个未初始化的int
decltype(*p) c;//错误:c是int&,是一个引用,必须初始化
decltype((i)) d;//错误:d是int&,是一个引用,必须初始化
decltype(i) e;//正确:e是一个未初始化的int

上述代码第2行:显然,如果decltype(r),则结果是一个引用类型,但是r+0是一个表达式,而不是一个单独的变量,所以返回的是r+0的结果类型,即int。

第3行:如果表达式的内容是解引用符,则decltype将得到引用类型。解引用指针可以得到指针所指的对象,而且还能给这个对象赋值。因此,decltype(*p)的结果类型是int &,而非int。

第4行:decltype与auto的另一处区别在于,decltype的结果类型与表达式形式密切相关。如果给变量加上了一层或多层括号,编译器会将其理解为表达式,而不是变量,变量是一种可以作为赋值语句左值的特殊表达式。记住,赋值是会产生引用的一类典型表达式,所以这里decltype会得到引用类型。

切记:decltype((variable))(注意是双层括号)的结果永远是引用,decltype(variable)结果只有当variable本身是引用才是引用。

29.默认初始化时,即对象未被显式地赋予初始值时,全局作用域的内置类型对象会默认初始化为0。局部作用域的对象未被初始化,其拥有未定义的值。如果试图拷贝该值或者以其他形式访问此类值将引发错误

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值