菜鸟笔记
提升您的技术认知

C++内功修炼----类型

 
本文将介绍 C++ 里面的
基本类型(引用 指针  结构体)
类型转换(static_cast、dynamic_cast、const_cast、reinterpret_cast),
类型处理(typedef auto decltype)

0.概述

1.基本类型

1.1 引用

1.1.1 引用的原理

引用的实现原理其实通过下面4句话,1副图,1段代码就可以说明白了 

int a=1;
int &b=a
1.引用变量b和被引用变量a并没有共用一块内存,b是另外开辟了一块内存的
2.引用变量b开辟的内存中存放的是a的地址 
3.任何对变量b的操作,都将转换为对(*b)的操作,比如b=b+1实际上是(*b)=(*b)+1 而(*b)代表的就是a 
4.基于上面3点我们可以总结出 引用变量b可以理解为被引用变量a的别名

上面的两句代码 对应的内存分布图就如下

再看一个实际的例子
 

#include<iostream>
using namespace std;
int main()
{
    int  a = 1;
    int&  b = a;
    cout << "a:address->" << &a << endl;
    cout << "b:address->" << &b << endl;

    getchar();
    return 0;
}

运行结果: 
a:address->0031FD54 
b:address->0031FD54

1.1.2引用的注意点

1.引用必须在声明引用时将其初始化,而不能先声明,再赋值。也不能在使用过程中途对其赋值企图更改被引用的值,那样是无效的
比如:
int rats = 101;
int & rodents = rats; 
int bunnies = 50;
rodents = bunnies;  
在上面一通操作以后rodent引用的还是rats
2.在用引用作为函数形参的时候,如果实参与引用参数不匹配,C++将生成临时参数。使用临时参数的数据改变不会影响传入的数据。
比如:

void swap(int &a,int &b)
{
    int temp;
 
    temp=a;
    a=b;
    b=temp;
}
 
long a=3,b=5;
swap(a,b);
这里的a,b与传入函数参数的a,b类型不匹配,因此编译器将创建两个临时int变量,将它们初始为3和5,然后交换临时变量的内容,而a和b保持不变。

1.1.3 引用的分类 

 (以下纯属个人理解 ,知识储备不够,如有错误,恳请指正)

在介绍引用的分类之前,先介绍一下两个概念 :左值和右值
先从C++98时代说起, 左值和右值区别如下,左值和右值最重要的特性就是:左值的持久状态的,右值是转瞬即逝的

 

在C++98时代我们只能对左值做引用 不能有右值引用,也就是上面所讲的那些引用.
但是在后来,我们有了一些新的需求
1. 我们想通过引用去改变临时变量的值 就好比实现下面的效果
 

std::string   s1     = "Hello ";
std::string   s2     = "world";
std::string& s_rref = s1 + s2;    // the result of s1 + s2 is an rvalue
  s_rref += ", my friend";           // I can change the temporary string!
std::cout << s_rref << '\n';       // prints "Hello world, my friend"

 所以引入了右值引用,用来绑定到一个转瞬即逝的右值上.
2,我们还有另外一个更大的想法,就是想用这种引入的右值引用来实现对象移动

我们知道在对象拷贝的时候 我们要先开辟一块内存然后把原来内存上的数据复制过去,然后释放掉原来的内存
这样效率有点低 ,我们用引用来实现对象的移动的想法是,用引用声明一个变量作为原来的对象的别名,然后销毁原来的对象(或者对象自己销毁自己),然后就可以通过引用来操作原来对象内存上的数据了,实现对象的移动.
在C++98时代 这种想法的实现是困难的,因为你用引用去绑定到一个对象以后,原来那个对象怎么自己销毁自己?
但是在C++11引入右值以后 我们就有新办法了,
就是把原来对象强行转换成右值,我们知道右值是转瞬即逝的,然后我们再定义一个右值引用绑定到该对象强行转换后的右值上
,
就大功告成了.
在C++11中,可以通过一个std::move()函数将左值对象转成右值对象 方便右值引用对其绑定,从而实现对象移动
像这种和右值引用相关的表达式,通常是将一个对象变成将要销毁的对象的右值 叫将亡值
而在C++11中的和C++98重复的那部分右值叫纯右值

左值引用就是对一个左值进行引用的类型。右值引用就是对一个右值进行引用的类型,事实上,由于右值通常不具有名字,我们也只能通过引用的方式找到它的存在。

右值引用和左值引用都是属于引用类型。无论是声明一个左值引用还是右值引用,都必须立即进行初始化。而其原因可以理解为是引用类型本身自己并不拥有所绑定对象的内存,只是该对象的一个别名。左值引用是具名变量值的别名,而右值引用则是不具名(匿名)变量的别名。
下表说明了左值引用右值引用对绑定量的限制

1.2指针

1.2.1指针的原理

  变量的结构和原理

说道变量,很多人都觉得非常简单,每天都在定义变量,应用变量。可是有没有停下脚步细细的品味一下具体什么是变量呢?变量(variable)的定义在计算机科学中到底是如何定义的?然后variable到底是在内存中如何存储值的呢?那么跟着上面的问题,我们来一一的解答,首先最重要的,variable的定义,当你申明一个变量的时候,计算机会将指定的一块内存空间和变量名进行绑定;这个定义很简单,但其实很抽象,例如:int x = 5; 这是一句最简单的变量赋值语句了, 我们常说“x等于5”,其实这种说法是错误的,x仅仅是变量的一个名字而已,它本身不等于任何值的。这条statement的正确翻译应该是:“将5赋值于名字叫做x的内存空间”,其本质是将值5赋值到一块内存空间,而这个内存空间名叫做x。切记:x只是简单的一个别名而已,x不等于任何值。其图示如下:

 变量在内存中的操作其实是需要经过2个步骤的:

1)找出与变量名相对应的内存地址。

2)根据找到的地址,取出该地址对应的内存空间里面的值进行操作。

 指针的结构和原理

首先介绍到底什么是指针?指针变量和任何变量一样,也有变量名,和这个变量名对应的内存空间,只是指针的特殊之处在于:指针变量相对应的内存空间存储的值恰好是某个内存地址。这也是指针变量区别去其他变量的特征之一。例如某个指针的定义如下:

int x = 5;
int *ptr = &x;

ptr即是一个指正变量名。通过指针获取这个指针指向的内存中的值称为dereference,这个的中文翻译叫啥我也不知道。【惭愧】,哈哈。dereference其相对于内存空间的表示如下:

特别提醒:这里千万千万不要钻进变量名x, ptr的牛角尖里面,不要去思考这些变量名存储在哪里,变量名仅仅是一块内存空间的代号名字而已,我们应该关心的是这些变量名相对应的内存地址。根据上面的分析可以看出,指针变量和任何变量在内存中的形式是相同的,仅仅在于其存储的值比较特殊而已。

常量指针和指针常量区别?

常量指针是一个指针,读成常量的指针,指向一个只读变量。如int const *p或const int *p。

指针常量是一个不能给改变指向的指针。如int *const p。
 

区别以下几种变量?

const int a;

int const a;

const int *a;

int *const a;

int const a和const int a均表示定义常量类型a。

const int *a,其中a为指向int型变量的指针,const在 * 左侧,表示a指向不可变常量。(看成const (*a),对引用加const)

int *const a,依旧是指针类型,表示a为指向整型数据的常指针。(看成const(a),对指针const)

区别以下指针类型?

int *p[10]

int (*p)[10]

int *p(int)

int (*p)(int)

int *p[10]表示指针数组,强调数组概念,是一个数组变量,数组大小为10,数组内每个元素都是指向int类型的指针变量。

int (*p)[10]表示数组指针,强调是指针,只有一个变量,是指针类型,不过指向的是一个int类型的数组,这个数组大小是10。

int *p(int)是函数声明,函数名是p,参数是int类型的,返回值是int *类型的。

int (*p)(int)是函数指针,强调是指针,该指针指向的函数具有int类型参数,并且返回值是int类型的。

指针数组和数组指针的区别?

指针数组:array of pointers,即用于存储指针的数组,也就是数组元素都是指针

数组指针:a pointer to an array,即指向数组的指针

指针不能解引用的情况?

但一个指针的值是invalid的时候,那么这个指针是不能dereference的。那么到底哪几种情况是invalid的呢?主要有以下几种情况:

1)当这个指针的值是NULL的时候,这个指针是不能dereference的。因为指针为NULL,即表示这个指针指向内存地址为0的地址块,内存地址为0的内存空间是没有值的,所以是不能dereference的; 例如:int *ptr = NULL; cout<<*ptr<<endl; 是错误的。

2)当某个指针被deallocte或者某个指针所在的内存空间被erase了的话,那么这个指针也是不能被dereference的;例如下面的代码:

int *function(int a){
    
    int temp = 5;
    return &temp;
}

上面的代码返回的指针也是不能dereference的,因为temp出了作用域后会被系统回收这一块空间,temp所占的内存空间已经被erase了,所以它返回的指针是一个指向被erase了的内存空间。也是不能dereference的,否则会出错。编译阶段会给出警告,在runtime的时候,如果dereference是会有error的。

1.2.2指针与引用

引用(reference)在C++中也是经常被用到,尤其是在作为函数参数的时候,需要在函数内部修改更新函数外部的值的时候,可以说是引用场景非常丰富。但程序员一般很难或者不注意分析reference和pointer,只是知道怎么应用而已,而不去具体分析这个reference。下面我就来简单的分析一下这个reference。首先我们必须明确的一点就是:reference是一种特殊的pointer。从这可以看出reference在内存中的存储结构应该跟上面的指针是一样的,也是存储的一块内存的地址。例如reference的定义如下:

int x = 5;
int &y = x;

reference 和 pointer主要有以下3中不同点:

1)reference不需要dereference即可直接获取到指向的内存空间的值。例如上例中,直接y就可以获取reference y所指向的内存空间的值,而不需要*y来获取。

2)reference的赋值操作也不需要取地址符来赋值,可以直接通过变量名,例如上例中,int &y = x, 而不需要 int &y = &x;

3) reference 在申明的时候就必须要有初始值,而且reference变量指向的内存地址是不能变化,不像pointer那样可以很灵活的重新指向其他地址。

reference和pointer在内存中的结构和关系如下图所示:

引用在定义的时候必须进行初始化,并且不能够改变。指针在定义的时候不一定要初始化,并且指向的空间可变。(注:不能有引用的值不能为NULL)

有多级指针,但是没有多级引用,只能有一级引用。

指针和引用的自增运算结果不一样。(指针是指向下一个空间,引用时引用的变量值加1)

sizeof 引用得到的是所指向的变量(对象)的大小,而sizeof 指针得到的是指针本身的大小。

引用访问一个变量是直接访问,而指针访问一个变量是间接访问。

使用指针前最好做类型检查,防止野指针的出现;

引用底层是通过指针实现的;

作为参数时也不同,传指针的实质是传值,传递的值是指针的地址;传引用的实质是传地址,传递的是变量的地址。

C++中的指针参数传递和引用参数传递

指针参数传递本质上是值传递,它所传递的是一个地址值。值传递过程中,被调函数的形式参数作为被调函数的局部变量处理,会在栈中开辟内存空间以存放由主调函数传递进来的实参值,从而形成了实参的一个副本(替身)。值传递的特点是,被调函数对形式参数的任何操作都是作为局部变量进行的,不会影响主调函数的实参变量的值(形参指针变了,实参指针不会变)。

引用参数传递过程中,被调函数的形式参数也作为局部变量在栈中开辟了内存空间,但是这时存放的是由主调函数放进来的实参变量的地址。被调函数对形参(本体)的任何操作都被处理成间接寻址,即通过栈中存放的地址访问主调函数中的实参变量(根据别名找到主调函数中的本体)。因此,被调函数对形参的任何操作都会影响主调函数中的实参变量。

引用传递和指针传递是不同的,虽然他们都是在被调函数栈空间上的一个局部变量,但是任何对于引用参数的处理都会通过一个间接寻址的方式操作到主调函数中的相关变量。而对于指针传递的参数,如果改变被调函数中的指针地址,它将应用不到主调函数的相关变量。如果想通过指针参数传递来改变主调函数中的相关变量(地址),那就得使用指向指针的指针或者指针引用。

从编译的角度来讲,程序在编译时分别将指针和引用添加到符号表上,符号表中记录的是变量名及变量所对应地址。指针变量在符号表上对应的地址值为指针变量的地址值,而引用在符号表上对应的地址值为引用对象的地址值(与实参名字不同,地址相同)。符号表生成之后就不会再改,因此指针可以改变其指向的对象(指针变量中的值可以改),而引用对象则不能修改。

什么情况用指针当参数,什么时候用引用,为什么?

  1. 使用引用参数的主要原因有两个:

程序员能修改调用函数中的数据对象

通过传递引用而不是整个数据–对象,可以提高程序的运行速度 

  1. 一般的原则: 
    对于使用引用的值而不做修改的函数:

如果数据对象很小,如内置数据类型或者小型结构,则按照值传递;

如果数据对象是数组,则使用指针(唯一的选择),并且指针声明为指向const的指针;

如果数据对象是较大的结构,则使用const指针或者引用,已提高程序的效率。这样可以节省结构所需的时间和空间;

如果数据对象是类对象,则使用const引用(传递类对象参数的标准方式是按照引用传递);

  1. 对于修改函数中数据的函数:

如果数据是内置数据类型,则使用指针

如果数据对象是数组,则只能使用指针

如果数据对象是结构,则使用引用或者指针

如果数据是类对象,则使用引用

 

引用和指针的区别?将“引用”作为函数参数有哪些特点?

  1. 传递引用给函数与传递指针的效果是一样的。这时,被调函数的形参就成为原来主调函数中的实参变量或对象的一个别名来使用,所以在被调函数中对形参变量的操作就是对其相应的目标对象(在主调函数中)的操作。
  2. 使用引用传递函数的参数,在内存中并没有产生实参的副本,它是直接对实参操作;而使用一般变量传递函数的参数,当发生函数调用时,需要给形参分配存储单元,形参变量是实参变量的副本;如果传递的是对象,还将调用拷贝构造函数。因此,当参数传递的数据较大时,用引用比用一般变量传递参数的效率和所占空间都好。

         使用指针作为函数的参数虽然也能达到与使用引用的效果,但是,在被调函数中同样要给形参分配存储单元,且需要重复使用"*指针变量名"的形式进行运算,这很容易产生错误且程序的阅读性较差;另一方面,在主调函数的调用点处,必须用变量的地址作为实参。而引用更容易使用,更清晰。

 

 

1.2.3指针与数组

数组名和指针(这里为指向数组首元素的指针)区别?

二者均可通过增减偏移量来访问数组中的元素。

数组名不是真正意义上的指针,可以理解为常指针,所以数组名没有自增、自减等操作。

当数组名当做形参传递给调用函数后,就失去了原有特性,退化成一般指针,多了自增、自减操作,但sizeof运算符不能再得到原数组的大小了。

数组和指针的区别?

  1. 数组在内存中是连续存放的,开辟一块连续的内存空间数组所占存储空间:sizeof(数组名);数组大小:sizeof(数组名)/sizeof(数组元素数据类型)
  2. 运算符sizeof 可以计算出数组的容量(字节数)。sizeof(p),p 为指针得到的是一个指针变量的字节数,而不是p 所指的内存容量。
  3. 编译器为了简化对数组的支持,实际上是利用指针实现了对数组的支持。具体来说,就是将表达式中的数组元素引用转换为指针加偏移量的引用。
  4. 在向函数传递参数的时候,如果实参是一个数组,那用于接受的形参为对应的指针。也就是传递过去是数组的首地址而不是整个数组,能够提高效率
  5. 在使用下标的时候,两者的用法相同,都是原地址加上下标值不过数组的原地址就是数组首元素的地址是固定的,指针的原地址就不是固定的

 

1.2.4指针与函数

  1. 什么是函数指针?

函数指针指向的是特殊的数据类型,函数的类型是由其返回的数据类型和其参数列表共同决定的,而函数的名称则不是其类型的一部分。

一个具体函数的名字,如果后面不跟调用符号(即括号),则该名字就是该函数的指针(注意:大部分情况下,可以这么认为,但这种说法并不很严格)。

  1. 函数指针的声明方法

int (*pf)(const int&, const int&); (1)

上面的pf就是一个函数指针,指向所有返回类型为int,并带有两个const int&参数的函数。注意*pf两边的括号是必须的,否则上面的定义就变成了:

int *pf(const int&, const int&); (2)

而这声明了一个函数pf,其返回类型为int *, 带有两个const int&参数。

  1. 为什么有函数指针

函数与数据项相似,函数也有地址。我们希望在同一个函数中通过使用相同的形参在不同的时间使用产生不同的效果。

  1. 一个函数名就是一个指针,它指向函数的代码。一个函数地址是该函数的进入点,也就是调用函数的地址。函数的调用可以通过函数名,也可以通过指向函数的指针来调用。函数指针还允许将函数作为变元传递给其他函数;
  2. 两种方法赋值:

 

1.2.5野指针

野指针是什么?
野指针:指向内存被释放的内存或者没有访问权限的内存的指针。

“野指针”的成因主要有3种:

指针变量没有被初始化。任何指针变量刚被创建时不会自动成为NULL指针,它的缺省值是随机的,它会乱指一气。

指针p被free或者delete之后,没有置为NULL;

指针操作超越了变量的作用范围。

 

如何避免野指针:

  • 对指针进行初始化
    将指针初始化为NULL
    Char *   p  = NULL;
    malloc分配内存
    char * p = (char * )malloc(sizeof(char));
    用已有合法的可访问的内存地址对指针初始化
    char num[ 30] = {0};
    char *p = num;
  • 指针用完后释放内存,将指针赋NULL。

delete(p);
p = NULL;

1.3结构体

C语言struct和C++struct区别?

C语言中:struct是用户自定义数据类型(UDT);C++中struct是抽象数据类型(ADT),支持成员函数的定义,(C++中的struct能继承,能实现多态)。

C中struct是没有权限的设置的,且struct中只能是一些变量的集合体,可以封装数据却不可以隐藏数据,而且成员不可以是函数。

C++中,struct的成员默认访问说明符为public(为了与C兼容),class中的默认访问限定符为private,struct增加了访问权限,且可以和类一样有成员函数。

struct作为类的一种特例是用来自定义数据结构的。一个结构标记声明后,在C中必须在结构标记前加上struct,才能做结构类型名

C++中的结构体与类的区别?

 (1)class中默认的成员访问权限是private的,而struct中则是public的。 

(2)class继承默认是private继承,而从struct继承默认是public继承。

 

结构体中的内存对齐? 

默认的对齐方式:各成员变量在存放的时候根据在结构中出现的顺序依次申请空间,同时按照上面的对齐方式调整位置,空缺的字节VC会自动填充。
同时VC为了确保结构的大小为结构的字节边界数(即该结构中占用最大空间的类型所占用的字节数)的倍数,所以在为最后一个成员变量申请空间后,还会根据需要自动填充空缺的字节。

注:VC对变量存储的一个特殊处理。为了提高CPU的存储速度,VC对一些变量的起始地址做了“对齐”处理。在默认情况下,VC规定各成员变量存放的起始地址相对于结构的起始地址的偏移量必须为该变量的类型所占用的字节数的倍数。

(1)示例代码一:


1 struct MyStruct
2 {
3     double dda1;
4     char dda;
5     int type;
6 };
7 //错:sizeof(MyStruct)=sizeof(double)+sizeof(char)+sizeof(int)=13。
8 //对:当在VC中测试上面结构的大小时,你会发现sizeof(MyStruct)为16。

注:为上面的结构分配空间的时候,VC根据成员变量出现的顺序和对齐方式。

(1)先为第一个成员dda1分配空间,其起始地址跟结构的起始地址相同(刚好偏移量0刚好为sizeof(double)的倍数),该成员变量占用sizeof(double)=8个字节;

(2)接下来为第二个成员dda分配空间,这时下一个可以分配的地址对于结构的起始地址的偏移量为8,是sizeof(char)的倍数,所以把dda存放在偏移量为8的地方满足对齐方式,
该成员变量占用sizeof(char)=1个字节;

(3)接下来为第三个成员type分配空间,
这时下一个可以分配的地址对于结构的起始地址的偏移量为9,
不是sizeof(int)=4的倍数,为了满足对齐方式对偏移量的约束问题,
VC自动填充3个字节(这三个字节没有放什么东西),
这时下一个可以分配的地址对于结构的起始地址的偏移量为12,
刚好是sizeof(int)=4的倍数,所以把type存放在偏移量为12的地方,
该成员变量占用sizeof(int)=4个字节;

这时整个结构的成员变量已经都分配了空间,总的占用的空间大小为:8+1+3+4=16,刚好为结构的字节边界数(即结构中占用最大空间的类型所占用的字节数sizeof(double)=8)的倍数,
所以没有空缺的字节需要填充。
所以整个结构的大小为:sizeof(MyStruct)=8+1+3+4=16,
其中有3个字节是VC自动填充的,没有放任何有意义的东西。

(2)示例代码二:交换一下上述例子中MyStruct的成员变量的位置

复制代码
1 struct MyStruct
2 {
3     char dda;
4     double dda1;
5     int type;
6 };
7 //错:sizeof(MyStruct)=sizeof(double)+sizeof(char)+sizeof(int)=13。
8 //对:当在VC中测试上面结构的大小时,你会发现sizeof(MyStruct)为24。

 

注:为上面的结构分配空间的时候,VC根据成员变量出现的顺序和对齐方式。

(1)先为第一个成员dda分配空间,
其起始地址跟结构的起始地址相同(刚好偏移量0刚好为sizeof(char)的倍数),
该成员变量占用sizeof(char)=1个字节;

(2)接下来为第二个成员dda1分配空间,
这时下一个可以分配的地址对于结构的起始地址的偏移量为1,
不是sizeof(double)=8的倍数,需要补足7个字节才能使偏移量变为8(满足对齐方式),
因此VC自动填充7个字节,dda1存放在偏移量为8的地址上,它占用8个字节;

(3)接下来为第三个成员type分配空间,
这时下一个可以分配的地址对于结构的起始地址的偏移量为16,
是sizeof(int)=4的倍数,满足int的对齐方式,所以不需要VC自动填充,
type存放在偏移量为16的地址上,该成员变量占用sizeof(int)=4个字节; 
这时整个结构的成员变量已经都分配了空间,总的占用的空间大小为:1+7+8+4=20,
不是结构的节边界数
(即结构中占用最大空间的类型所占用的字节数sizeof(double)=8)的倍数,
所以需要填充4个字节,以满足结构的大小为sizeof(double)=8的倍数。
所以该结构总的大小为:sizeof(MyStruct)为1+7+8+4+4=24。
其中总的有7+4=11个字节是VC自动填充的,没有放任何有意义的东西。

 



字节的对齐方式:

在VC中提供了#pragmapack(n)来设定变量以n字节对齐方式。n字节对齐就是说变量存放的起始地址的偏移量有两种情况:
第一,如果n大于等于该变量所占用的字节数,那么偏移量必须满足默认的对齐方式;
第二,如果n小于该变量的类型所占用的字节数,那么偏移量为n的倍数,不用满足默认的对齐方式。
结构的总大小也有个约束条件,
分下面两种情况:如果n大于所有成员变量类型所占用的字节数,
那么结构的总大小必须为占用空间最大的变量占用的空间数的倍数;否则必须为n的倍数。

注:VC对结构的存储的特殊处理确实提高了CPU存储变量的速度,
但有时也会带来一些麻烦,我们也可以屏蔽掉变量默认的对齐方式,自己来设定变量的对齐方式。

(1)示例代码:


 1 #pragmapack(push)//保存对齐状态
 2 
 3 
 4 #pragmapack(4)//设定为4字节对齐
 5 
 6 struct test
 7 {
 8     char m1;
 9     double m4;
10     int m3;
11 };
12 
13 #pragmapack(pop)//恢复对齐状态
复制代码
 

注:以上结构的大小为16,下面分析其存储情况。

(1)首先为m1分配空间,其偏移量为0,满足我们自己设定的对齐方式(4字节对齐),m1占用1个字节;

(2)接着开始为m4分配空间,这时其偏移量为1,需要补足3个字节,这样使偏移量满足为n=4的倍数(因为sizeof(double)大于n),m4占用8个字节;

(3)接着为m3分配空间,这时其偏移量为12,满足为4的倍数,m3占用4个字节;
 这时已经为所有成员变量分配了空间,共分配了16个字节,满足为n的倍数。
如果把上面的#pragmapack(4)改为#pragma pack(8),
那么我们可以得到结构的大小为24。

 

为什么要内存对齐?

平台移植型好
不是所有的硬件平台都能访问任意地址上的数据;某些硬件平台只能只在某些地址访问某些特定类型的数据,否则抛出硬件异常,及遇到未对齐的边界直接就不进行读取数据了。

cpu处理效率高


从上图可以看出,对应两种存储方式,若CPU的读取粒度为4字节,

那么对于一个int 类型,若是按照内存对齐来存储,处理器只需要访存一次就可以读取完4个字节
若没有按照内存对其来读取,如上图所示,就需要访问内存两次才能读取出一个完整的int 类型变量
具体过程为,第一次拿出 4个字节,丢弃掉第一个字节,第二次拿出4个字节,丢弃最后的三个字节,然后拼凑出一个完整的 int 类型的数据。
其实结构体内存对齐是拿空间换取时间的做法。提高效率

2.类型转换

2.1 隐式类型转换
 

在C++基本的数据类型中,可以分为四类:整型,浮点型,字符型,布尔型。其中数值型包括 整型与浮点型;字符型即为char。

(1)将浮点型数据赋值给整型变量时,舍弃其小数部分。

(2)将整型数据赋值给浮点型变量时,数值不变,但是以指数形式存储。

(3)将double型数据赋值给float型变量时,注意数值范围溢出。

(4)字符型数据可以赋值给整型变量,此时存入的是字符的ASCII码。

(5)将一个int,short或long型数据赋值给一个char型变量,只将低8位原封不动的送到char型变量中。 
(6)将有符号型数据赋值给长度相同的无符号型变量,连同原来的符号位一起传送。

      1.  隐式转换,如何消除隐式转换?
      2. C++的基本类型中并非完全的对立,部分数据类型之间是可以进行隐式转换的。所谓隐式转换,是指不需要用户干预编译器私下进行的类型转换行为。很多时候用户可能都不知道进行了哪些转换
      3.  C++面向对象的多态特性,就是通过父类的类型实现对子类的封装。通过隐式转换,你可以直接将一个子类的对象使用父类的类型进行返回。在比如,数值和布尔类型的转换,整数和浮点数的转换等。某些方面来说,隐式转换给C++程序开发者带来了不小的便捷。C++是一门强类型语言,类型的检查是非常严格的。
      4. 基本数据类型 基本数据类型的转换以取值范围的作为转换基础(保证精度不丢失)。隐式转换发生在从小->大的转换中。比如从char转换为int。从int->long。自定义对象 子类对象可以隐式的转换为父类对象。
      5. C++中提供了explicit关键字,在构造函数声明的时候加上explicit关键字,能够禁止隐式转换。
      6. 如果构造函数只接受一个参数,则它实际上定义了转换为此类类型的隐式转换机制。可以通过将构造函数声明为explicit加以制止隐式类型转换,关键字explicit只对一个实参的构造函数有效,需要多个实参的构造函数不能用于执行隐式转换,所以无需将这些构造函数指定为explicit

2.2 强制类型转换
 

static_cast、const_cast、reinterpret_cast和dynamic_cast 

1) static_cast
在C++语言中static_cast用于数据类型的强制转换,强制将一种数据类型转换为另一种数据类型。例如将整型数据转换为浮点型数据。
[例1]C语言所采用的类型转换方式:

int a = 10;
int b = 3;
double result = (double)a / (double)b;
 

例1中将整型变量a和b转换为双精度浮点型,然后相除。在C++语言中,我们可以采用static_cast关键字来进行强制类型转换,如下所示。
[例2]static_cast关键字的使用:

int a = 10;
int b = 3;
double result = static_cast<double>(a) / static_cast<double>(b);
 

在本例中同样是将整型变量a转换为双精度浮点型。采用static_cast进行强制数据类型转换时,将想要转换成的数据类型放到尖括号中,将待转换的变量或表达式放在元括号中,其格式可以概括为如下形式:    

用法:static_cast <类型说明符> (变量或表达式)

它主要有如下几种用法:
    (1)用于类层次结构中基类和派生类之间指针或引用的转换
      进行上行转换(把派生类的指针或引用转换成基类表示)是安全的
      进行下行转换(把基类的指针或引用转换为派生类表示),由于没有动态类型检查,所以是不安全的
    (2)用于基本数据类型之间的转换,如把int转换成char。这种转换的安全也要开发人员来保证
    (3)把空指针转换成目标类型的空指针
    (4)把任何类型的表达式转换为void类型
    注意:static_cast不能转换掉expression的const、volitale或者__unaligned属性。

static_cast:可以实现C++中内置基本数据类型之间的相互转换。

如果涉及到类的话,static_cast只能在有相互联系的类型中进行相互转换,不一定包含虚函数。

 

 

2) const_cast
在C语言中,const限定符通常被用来限定变量,用于表示该变量的值不能被修改。

而const_cast则正是用于强制去掉这种不能被修改的常数特性,但需要特别注意的是const_cast不是用于去除变量的常量性,而是去除指向常数对象的指针或引用的常量性,其去除常量性的对象必须为指针或引用。

用法:const_cast<type_id> (expression)
    该运算符用来修改类型的const或volatile属性。除了const 或volatile修饰之外, type_id和expression的类型是一样的。
    常量指针被转化成非常量指针,并且仍然指向原来的对象;
    常量引用被转换成非常量引用,并且仍然指向原来的对象;常量对象被转换成非常量对象。

[例3]一个错误的例子:

const int a = 10;
const int * p = &a;
*p = 20;                  //compile error
int b = const_cast<int>(a);  //compile error
在本例中出现了两个编译错误,第一个编译错误是*p因为具有常量性,其值是不能被修改的;另一处错误是const_cast强制转换对象必须为指针或引用,而例3中为一个变量,这是不允许的!

[例4]const_cast关键字的使用

#include<iostream>
using namespace std;
 
int main()
{
    const int a = 10;
    const int * p = &a;
    int *q;
    q = const_cast<int *>(p);
    *q = 20;    //fine
    cout <<a<<" "<<*p<<" "<<*q<<endl;
        cout <<&a<<" "<<p<<" "<<q<<endl;
    return 0;
}
在本例中,我们将变量a声明为常量变量,同时声明了一个const指针指向该变量(此时如果声明一个普通指针指向该常量变量的话是不允许的,Visual Studio 2010编译器会报错)。

之后我们定义了一个普通的指针*q。将p指针通过const_cast去掉其常量性,并赋给q指针。之后我再修改q指针所指地址的值时,这是不会有问题的。

最后将结果打印出来,运行结果如下:
10 20 20
002CFAF4 002CFAF4 002CFAF4

查看运行结果,问题来了,指针p和指针q都是指向a变量的,指向地址相同,而且经过调试发现002CFAF4地址内的值确实由10被修改成了20,这是怎么一回事呢?为什么a的值打印出来还是10呢?

其实这是一件好事,我们要庆幸a变量最终的值没有变成20!变量a一开始就被声明为一个常量变量,不管后面的程序怎么处理,它就是一个常量,就是不会变化的。试想一下如果这个变量a最终变成了20会有什么后果呢?对于这些简短的程序而言,如果最后a变成了20,我们会一眼看出是q指针修改了,但是一旦一个项目工程非常庞大的时候,在程序某个地方出现了一个q这样的指针,它可以修改常量a,这是一件很可怕的事情的,可以说是一个程序的漏洞,毕竟将变量a声明为常量就是不希望修改它,如果后面能修改,这就太恐怖了。

在例4中我们称“*q=20”语句为未定义行为语句,所谓的未定义行为是指在标准的C++规范中并没有明确规定这种语句的具体行为,该语句的具体行为由编译器来自行决定如何处理。对于这种未定义行为的语句我们应该尽量予以避免!

从例4中我们可以看出我们是不想修改变量a的值的,既然如此,定义一个const_cast关键字强制去掉指针的常量性到底有什么用呢?我们接着来看下面的例子。

例5:

#include<iostream>
using namespace std;
 
const int * Search(const int * a, int n, int val);
 
int main()
{
    int a[10] = {0,1,2,3,4,5,6,7,8,9};
    int val = 5;
    int *p;
    p = const_cast<int *>(Search(a, 10, val));
    if(p == NULL)
        cout<<"Not found the val in array a"<<endl;
    else
        cout<<"hvae found the val in array a and the val = "<<*p<<endl;
    return 0;
}
 
const int * Search(const int * a, int n, int val)
{
    int i;
    for(i=0; i<n; i++)
    {
        if(a[i] == val)
            return &a[i];
    }
    return  NULL;
}
 

在例5中我们定义了一个函数,用于在a数组中寻找val值,如果找到了就返回该值的地址,如果没有找到则返回NULL。函数Search返回值是const指针,当我们在a数组中找到了val值的时候,我们会返回val的地址,最关键的是a数组在main函数中并不是const,因此即使我们去掉返回值的常量性有可能会造成a数组被修改,但是这也依然是安全的。

对于引用,我们同样能使用const_cast来强制去掉常量性,如例6所示。

例6:


#include<iostream>
using namespace std;
 
const int & Search(const int * a, int n, int val);
 
int main()
{
int a[10] = {0,1,2,3,4,5,6,7,8,9};
int val = 5;
int &p = const_cast<int &>(Search(a, 10, val));
if(p == NULL)
cout<<"Not found the val in array a"<<endl;
else
cout<<"hvae found the val in array a and the val = "<<p<<endl;
return 0;
}
 
const int & Search(const int * a, int n, int val)
{
int i;
for(i=0; i<n; i++)
{
if(a[i] == val)
return a[i];
}
return NULL;
}
 了解了const_cast的使用场景后,可以知道使用const_cast通常是一种无奈之举,同时也建议大家在今后的C++程序设计过程中一定不要利用const_cast去掉指针或引用的常量性并且去修改原始变量的数值,这是一种非常不好的行为。
3) reinterpret_cast
在C++语言中,reinterpret_cast主要有三种强制转换用途:改变指针或引用的类型、将指针或引用转换为一个足够长度的整形、将整型转换为指针或引用类型。

用法:reinterpret_cast<type_id> (expression)
    type-id必须是一个指针、引用、算术类型、函数指针或者成员指针。
    它可以把一个指针转换成一个整数,也可以把一个整数转换成一个指针(先把一个指针转换成一个整数,在把该整数转换成原类型的指针,还可以得到原先的指针值)。
    在使用reinterpret_cast强制转换过程仅仅只是比特位的拷贝,因此在使用过程中需要特别谨慎!

例7:

int *a = new int;
double *d = reinterpret_cast<double *>(a);
在例7中,将整型指针通过reinterpret_cast强制转换成了双精度浮点型指针。
reinterpret_cast可以将指针或引用转换为一个足够长度的整形,此中的足够长度具体长度需要多少则取决于操作系统,如果是32位的操作系统,就需要4个字节及以上的整型,如果是64位的操作系统则需要8个字节及以上的整型。 

 

4) dynamic_cast
 用法:dynamic_cast<type_id> (expression)

 

(1)其他三种都是编译时完成的,dynamic_cast是运行时处理的,运行时要进行类型检查。

(2)不能用于内置的基本数据类型的强制转换。

(3)dynamic_cast转换如果成功的话返回的是指向类的指针或引用,转换失败的话则会返回NULL。

(4)使用dynamic_cast进行转换的,基类中一定要有虚函数,否则编译不通过。

        B中需要检测有虚函数的原因:类中存在虚函数,就说明它有想要让基类指针或引用指向派生类对象的情况,此时转换才有意义。

       
        这是由于运行时类型检查需要运行时类型信息,而这个信息存储在类的虚函数表(关于虚函数表的概念,详细可见<Inside c++ object model>)中,
        只有定义了虚函数的类才有虚函数表。

 (5)在类的转换时,在类层次间进行上行转换时,dynamic_cast和static_cast的效果是一样的。在进行下行转换时,dynamic_cast具有类型检查的功能,比static_cast更安全。

        向上转换,即为子类指针指向父类指针(一般不会出问题);向下转换,即将父类指针转化子类指针。

       向下转换的成功与否还与将要转换的类型有关,即要转换的指针指向的对象的实际类型与转换以后的对象类型一定要相同,否则转换失败。

        在C++中,编译期的类型转换有可能会在运行时出现错误,特别是涉及到类对象的指针或引用操作时,更容易产生错误。Dynamic_cast操作符则可以在运行期对可能产生问题的类型转换进行测试。

例1:

#include<iostream>
using namespace std;
 
class base
{
public :
    void m(){cout<<"m"<<endl;}
};
 
class derived : public base
{
public:
    void f(){cout<<"f"<<endl;}
};
 
int main()
{
    derived * p;
    p = new base;
    p = static_cast<derived *>(new base);
    p->m();
    p->f();
    return 0;
}
本例中定义了两个类:base类和derived类,这两个类构成继承关系。在base类中定义了m函数,derived类中定义了f函数。在前面介绍多态时,我们一直是用基类指针指向派生类或基类对象,而本例则不同了。

本例主函数中定义的是一个派生类指针,当我们将其指向一个基类对象时,这是错误的,会导致编译错误。

但是通过强制类型转换我们可以将派生类指针指向一个基类对象,p = static_cast<derived *>(new base);语句实现的就是这样一个功能,这样的一种强制类型转换时合乎C++语法规定的,但是是非常不明智的,它会带来一定的危险。

在程序中p是一个派生类对象,我们将其强制指向一个基类对象,首先通过p指针调用m函数,因为基类中包含有m函数,这一句没有问题,之后通过p指针调用f函数。一般来讲,因为p指针是一个派生类类型的指针,而派生类中拥有f函数,因此p->f();这一语句不会有问题,但是本例中p指针指向的确实基类的对象,而基类中并没有声明f函数,虽然p->f();这一语句虽然仍没有语法错误,但是它却产生了一个运行时的错误。换言之,p指针是派生类指针,这表明程序设计人员可以通过p指针调用派生类的成员函数f,但是在实际的程序设计过程中却误将p指针指向了一个基类对象,这就导致了一个运行期错误。

产生这种运行期的错误原因在于static_cast强制类型转换时并不具有保证类型安全的功能,而C++提供的dynamic_cast却能解决这一问题,dynamic_cast可以在程序运行时检测类型转换是否类型安全。

当然dynamic_cast使用起来也是有条件的,它要求所转换的操作数必须包含多态类类型(即至少包含一个虚函数的类)。

 

例2:

#include<iostream>
using namespace std;
 
class base
{
public :
    void m(){cout<<"m"<<endl;}
};
 
class derived : public base
{
public:
    void f(){cout<<"f"<<endl;}
};
 
int main()
{
    derived * p;
    p = new base;
    p = dynamic_cast<derived *>(new base);
    p->m();
    p->f();
    return 0;
}
在本例中利用dynamic_cast进行强制类型转换,但是因为base类中并不存在虚函数,因此p = dynamic_cast<derived *>(new base);这一句会编译错误。

为了解决本例中的语法错误,我们可以将base类中的函数m声明为虚函数,virtual void m(){cout<<"m"<<endl;}。

dynamic_cast还要求<>内部所描述的目标类型必须为指针或引用。

 

例3:

#include<iostream>
#include<cstring>
 
using namespace std;
 
class A
{
   public:
   virtual void f()
   {
       cout<<"hello"<<endl;
       };
};
 
  
 
class B:public A
{
    public:
    void f()
    {
        cout<<"hello2"<<endl;
        };
  
};
 
  
 
class C
{
  void pp()
  {
      return;
  }
};
 
  
 
int fun()
{
    return 1;
}
 
int main()
{
    A* a1=new B;//a1是A类型的指针指向一个B类型的对象
    A* a2=new A;//a2是A类型的指针指向一个A类型的对象
    B* b;
    C* c;
    b=dynamic_cast<B*>(a1);//结果为not null,向下转换成功,a1之前指向的就是B类型的对象,所以可以转换成B类型的指针。
    if(b==NULL)
    {
        cout<<"null"<<endl;
    }
 
    else
    {
        cout<<"not null"<<endl;
    }
 
    b=dynamic_cast<B*>(a2);//结果为null,向下转换失败
    if(b==NULL)
    {
        cout<<"null"<<endl;
    }
 
    else
    {
        cout<<"not null"<<endl;
    }
 
    c=dynamic_cast<C*>(a);//结果为null,向下转换失败
    if(c==NULL)
    {
        cout<<"null"<<endl;
    }
 
    else
    {
        cout<<"not null"<<endl;
    }
 
    delete(a);
    return 0;
}

string类提供的方法 


标准库的string类提供了3个成员函数来从一个string得到c类型的字符数组:
c_str()、data()、copy(p,n)。

1. c_str():生成一个const char*指针,指向以空字符终止的数组。

注: 
①这个数组的数据是临时的,当有一个改变这些数据的成员函数被调用后,其中的数据就会失效。因此要么现用先转换,要么把它的数据复制到用户自己可以管理的内存中。注意。看下例:


const char* c;
string s="1234";
c = s.c_str(); 
cout<<c<<endl; //输出:1234
s="abcd";
cout<<c<<endl; //输出:abcd

  上面如果继续用c指针的话,导致的错误将是不可想象的。就如:1234变为abcd 
  其实上面的c = s.c_str(); 不是一个好习惯。既然c指针指向的内容容易失效,我们就应该按照上面的方法,那怎么把数据复制出来呢?这就要用到strcpy等函数(推荐)。 
代码实现如下:

#include <iostream>
#include <string>
#include <exception>
using namespace std;


int main () 
{
    char a[5]={'1','2','3','4',0};
    string s="2468";
    char b[5];   //b数组长度>=a数组长度 b数组长度>=s字符串长度+1

    strcpy(b,a);      //字符串数组copy到另一个字符串数组
    cout<<b<<endl;

    string s1(b,b+2);    //字符串数组->string 并且可以指定位置  前闭后开
    cout<<s1<<endl;

    strcpy(b,s.c_str());  //string->字符串数组
    cout<<b<<endl;

    string s2(s1.substr(0,2));    //string ->string  指定位置
    cout<<s2<<endl;

    string s3(s2.c_str());    //s2转换为字符数组  再赋值给string
    cout<<s3<<endl;

    string s4(s3);           //直接初始化
    cout<<s4<<endl;

    return 0;

}

② c_str()返回一个客户程序可读不可改的指向字符数组的指针,不需要手动释放或删除这个指针。

2、data():与c_str()类似,但是返回的数组不以空字符终止。

3、copy(p,n,size_type _Off = 0):从string类型对象中至多复制n个字符到字符指针p指向的空间中。默认从首字符开始,但是也可以指定,开始的位置(记住从0开始)。返回真正从对象中复制的字符。——用户要确保p指向的空间足够保存n个字符。

3.类型处理

1.typedef类型别名

类型别名是一个名字,它是某种类型的同义词,使用它让复杂的类型名字变得简单明了,易于理解和使用,还有助于程序员清楚的知道使用该类型的真实目的。

定义类型别名有两种方法:

1:用关键字typedef定义,关键字typedef作为声明语句的基本数据类型的一部分出现,这里的声明符也可以包含类型修饰,从而也能由基本数据类型构造出符合类型来。

2:新标准使用了一种新的方法,使用关键字using作为别名声明的开始,气候紧跟别名和等号,作用是把等号左侧的名字规定成等号右侧类型的别名。


typedef double wages;               //wages是double的同义词
typedef wages base, *p             //base是double的同义词,p是double *的同义词</span>
注意,如果某个类型别名指代的是复合类型或常量,那么把它用到声明语句里就会产生意想不到的后果。

typedef char  *pstring;     //pstring是char*的别名
const pstring cstr=0;        //pstring 实际上是指向char的指针,因此,const pstring就是指向char的常量指针,而非指向常                                               量字符的指针
const pstring *ps;            //ps是一个指针,他指向的对象是指向char的常量指针
const char *cstr=0;         //往往我们会倾向于把类型别名替换成它原来的样子,这样是错误的,这样改写的结果是,const char成了基本类型,它成了一个指向const char的指针,而愿意是指向char的常量指针,显然二者区别明显。

2.auto类型说明符
编程时常常需要把表达式的值赋给变量,这就需要在声明变量时知道表达式计算出的结果的类型,然而有时候知道这个类型却是很麻烦,为此C++11新标准引入了auto类型说明符,用它就能让编译器替我们去分析表达式所属的类型,显然一个前提是,auto定义的变量必须有初始值。

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


auto item=val1+val2;      //有二者相加的结果推断出item的类型
auto i=0,*p=&i;              //正确:i是整数,P是整型指针
auto sz=0,pi=3.14;         //错误:sz和pi的类型不一致
由编译器推断出来的auto类型有时候和初始值的类型也不完全一样,编译器会作适当修改:
1.当使用引用作初始值类型时,此时auto的类型是引用对象的类型。

2.auto一般会忽略掉顶层const,同时底层const则会保留下来。

int i=0,&r=i;
auto a=r;                      //a是一个整型(初始值的引用类型被忽略)
 
const int ci=i,&cr=ci;
auto b=ci;                   //b是一个整型(顶层const特性被忽略)
auto c=cr;                   //c是一个整型(首先忽略掉引用类型,再忽略掉顶层const类型)
auto d=&i                   //d是一个整型指针(此时的d前的指针可加可不加)
auto e=&ci                 //e是一个指向整型常量的指针
 
const auto f=ci;         //f是一个整型常量,如果希望推断出的auto类型是个顶层const,需要明确声明
 
//可以将引用的类型设置为auto,原初始化规则任适用
auto &g=ci;              //g是一个整型常量引用
auto &h=42;             //错误:不能为非常量引用绑定字面值
const auto &j=42     //正确
 
//要在一条语句中定义多个变量,切记,符号&和*只从属于某个声明符,而非基本数据类型的一部分,因此初始值必须是同一类型
auto k=ci,&m=i;
auto &n=ci,*p=&ci;
auto &q=i,*s=&ci;    //错误:i的类型时int型而&ci的类型却是const int 型

3.decltype类型指示符

有时我们希望从表达式的类型推断出要定义的变量的类型,但是不想用该表达式的值初始化变量。为满足这个要求,C++11引入了类型说明符decltype,它的作用是选择并返回操作数的数据类型,此过程中,编译器分析表达式并得到它的类型,却不实际计算表达式的值。

decltype(f()) sum=x;            //sum的类型就是函数f的返回类型
decltype处理顶层const和引用的方式与auto有些许不同,如果decltype使用的表达式是一个变量,则decltype返回该变量的类型(包括顶层const和引用在内):

const int ci=0,&cj=ci;
decltype(ci) x=0;                 //x的类型是const int
decltype(cj) y=x;                 //y的类型是const int&,y绑定到x
decltype(cj) z;                     //错误:z是一个引用,必须初始化
 
int i=42,*p=&i,&r=i;
decltype(r)  a=i;                  //a的类型是引用
decltype(r+0) b //b的类型是int,因为虽然r是引用,可是作为表达式的一部分,结果是int类型
decltype(*p) c  //错误:c是int&型,必须初始化,因为如果表达式的内容是解引用操作,decltype得到引用类型//对于decltype来说,如果在变量名上加一对括号,得到的类型与不加括号时的可能有所不同
decltype((i)) d; //错误:d是int&型,必须初始化
decltype (i) e; //正确:e是一个int型值

4.其他问题

怎样判断两个浮点数是否相等?

对两个浮点数判断大小和是否相等不能直接用==来判断,会出错!明明相等的两个数比较反而是不相等!对于两个浮点数比较只能通过相减并与预先设定的精度比较,记得要取绝对值!浮点数与0的比较也应该注意。与浮点数的表示方式有关。
 

在不使用额外空间的情况下,交换两个数?

算术

x = x + y;
y = x - y;

x = x - y; 

异或

x = x^y;// 只能对int,char..
y = x^y;
x = x^y;
x ^= y ^= x;

 

分别写出BOOL,int,float,指针类型的变量a 与“零”的比较语句。

BOOL : if ( !a ) or if(a)

int : if ( a == 0)

float : const EXPRESSION EXP = 0.000001

if ( a < EXP && a >-EXP)

pointer : if ( a != NULL) or if(a == NULL)

 

无论是float还是double类型的变量,都有精度限制。所以一定要避免将浮点变量用“==”或“!=”与数字比较,应该设法转化成“>=”或“<=”形式。

42、如何判断浮点数是否相等,LONG 呢

我们在判断浮点数相等时,推荐用范围来确定,若两个浮点数差值在某一范围内,我们就认为相等,至于

范围怎么定义,要看实际情况而已了,float 和 double 各有不同。

long 类型,没有精度问题,可以直接比较。