页面加载中 . . .

C/C++指针


指针的基本概念

1)变量的地址

变量是内存变量的简称,在C++中,系统会给变量分配一块内存,内存是有地址的。

0x00000001
0x00000002
0x00000003
0x00000004
0x00000005
0x00000006
0x00000007
0x00000008
0x00000009
0x0000000A
0x0000000B
……
0xFFFFFFFF

C++用运算符&获取变量在内存中的起始地址。

语法:&变量名

2)指针变量

指针变量简称指针,他是一种特殊的变量,专用于存放变量在内存中的起始地址

语法:数据类型 *变量名;

数据类型必须是合法的C++数据类型(int、char、double或其他自定义数据类型)。

星号*与乘法中的星号是相同的,但是,在这个场景中,星号用于表示这个变量是指针。

3)对指针赋值

不管是整型、浮点型、字符型,还是其他数据类型的变量,它的地址都是一个十六进制数。我们用整型指针存放整型变量的地址,用字符型指针存放字符型变量的地址,用浮点型指针存放浮点型变量的地址,用自定义数据类型指针存放自定义数据类型变量的地址。

语法:指针=&变量名;

4)指针占用的内存

指针也是变量,是变量就要占用内存空间。

在64位操作系统中,不管什么类型的指针,占用内存都是8字节。

在C++中,指针是复合数据类型,复合数据类型是指基于其他类型而定义的数据类型,在程序中,int是整型变量,int *是整型指针类型,int *可以用于sizeof运算符,可以用于数据类型的强制转换,总的来说,把int *当成一种数据类型就对了。

使用指针

声明变量后,在没有赋值之前,里面是乱七八糟的值,这时候不能使用指针。

指针存放变量的地址,因此,指针名表示的是地址(就像变量名可以表示变量的值一样)。

运算符被称为间接值解除引用(解引用),将它用于指针,可以的得到该地址的内存中存储的值,也是乘法符号,C++根据上下文来确定所指的是乘法还是解引用。

变量和指向变量的指针就像同一枚硬币的两面。

int no = 38;

int *ptr = &no;

程序在存储数据的时候,必须跟踪三种基本属性:

  • 程序存储在哪里;
  • 数据是什么类型;
  • 数据的值是多少。

用两种策略可以达到以上目的;

声明一个普通变量,声明时指出数据类型和变量名(符号名),系统在内部跟踪该内存单元。

声明一个指针变量,存储的值是地址,而不是值本身,程序直接访问该内存单元。

指针用于函数的参数

如果把函数的形参声明为指针,调用的时候把实参地址传进去,形参中存放的是实参的地址,在函数中

通过解引用的方法直接操作内存中的数据,可以修改实数的值,这种方法被通俗的称为地址传递传地址

值传递:函数的形参是普通变量。

传地址的意义如下:

  • 可以在函数中修改实参的值。
  • 减少内存拷贝,提升性能。

使用常量

常量是程序中固定不变的数据。

1)宏常量

一般在 main 函数的上面声明,用大写命名。

语法:#define 常量名 值

2)const修饰的变量

在程序的任何地方都可以声明。

语法:const 数据类型 常量名 = 值

3)常量的特点

程序中不允许改变常量的值,否则编译的时候会报错。

4)示例

#include <iostream>    //包含头文件

#define MONTHS 12           //一年中的月份数
#define PI     3.14159      //圆周率

using namespace std;        //指定缺省的命名空间

//main函数u,程序从这里开始执行,每个程序只能有一个 main 函数。
int main()
{
    const days = 7;        //一星期的天数

    cout << "一年有" << MONTHS << "个月" << endl;

}  

用const修饰指针

1)常量指针

语法:const 数据类型 *变量名;

不能通过解引用的方法修改内存地址中的值(用原始的变量名是可以修改的)。

注意:

  • 指向的变量(对象)可以改变(之前是指向变量a的,后来可以改为指向变量b)。
  • 一般用于修饰函数的形参,表示不希望在函数里修改内存地址中的值。
  • 如果用于形参、虽然指向的对象可以改变,但那么做没有任何意义。
  • 如果形参的值不需要改变,建议加上const修饰,程序可读性更好。
int a = 3, b = 8;
const int* p = &a;
//不能通过解引用赋值修改值,编译会报错 错误  C3892
//*p = 13;
//只能通过变量修改值
a = 13;
cout << "a=" << a << ",*p=" << *p << endl;
p = &b;
cout << "b=" << b << ",*p=" << *p << endl;
void func(const int* no,const string *str)
{
    //参数加上const,解引用赋值,编译时 同样会报错
    //*no = 8;
    //*str = "我有一只小小鸟.";
    cout << "亲爱的" << *no << "号" << *str << endl;
}

2)指针常量

语法:数据类型 *const 变量名;

指向的变量(对象)不可改变。

注意:

  • 在定义的同时必须初始化,否则没有意义。
  • 可以通过解引用的方法修改内存地址中的值。
  • C++编译器把指针常量做了一些特别的处理,改投换面之后,有了一个新的名字,叫做引用。

3)常指针常量

语法:const 数据类型 *const 变量名;

指向的变量(对象)不可改变,不能通过解引用的方法修改内存地址中的值。

//常指针常量
int a = 3,b = 8;
const int* const pa = &a;
//既不能改变指针指向,也不能解引用改变值,下列两种写法都是错误的
//*pa = 13;
//pa = &b;

常量指针:指针指向可以改,指针指向的值不可以改。

指针常量:指针指向不可以改,指针的指向的值可以更改。

常指针常量:指针指向不可以改,指针指向的值不可以更改。

void 关键字

在C++中,void表示为无类型,主要有三个用途:

  • 函数的返回值使用 void ,表示函数没有返回值。

    void func (int a,int b)
    {
         //函数体代码
         return;   
    }
  • 函数的参数填 void ,表示函数不需要参数(或者让参数列表空着)。

    int func( void )
    {
        //函数体代码
        return 0;
    }
  • 函数的形参用 void *,表示接受任意数据类型的指针。

    注意:

    • 不能用 void声明变量,它不能代表一个真实的变量。
    • 不能对 void *指针直接解引用(需要转化为其他类型的指针)。
    • 把其他类型的指针赋值给 void *指针不需要转换。
    • void *指针赋值给其他类型的指针需要转换。

动态分配内存new和delete

C++内存空间

使用堆区内存有四个步骤:

1)声明一个指针;

2)用new运算符向系统申请一块内存,让指针指向这块内存;

3)通过对指针的解引用的方法,像使用变量一样使用这块内存;

4)如果这块内存不用了,用delete运算符释放它;

申请内存的语法:new 数据类型(初始值); //C++11支持 {}

如果申请成功,返回一个地址;如果申请失败,返回一个空地址(暂时不考虑失败的情况)

是否内存的语法:delete 地址;

注意:

  • 动态分配出来的内存没有变量名,只能通过指向它的指针来操作内存中的数据。
  • 如果动态分配的内存用不了,必须使用 delete释放它,否则可能用尽系统的内存。
  • 动态分配的内存的生命周期与程序相同,程序退出时,如果没有是否,系统将自动回收。
  • 就算指针的作用域已失效,所指向的内存也不会释放。
  • 用指针跟踪已分配的内存时,不能跟丢。

二级指针

指针指针变量的简称,也是变量。是变量就有地址

指针用于存放普通变量地址

二级指针用于存放指针变量地址

声明二级指针的语法:数据类型** 指针名;

使用指针有两个目的:1)传递地址;2)存放动态分配的内存的地址。

在函数中,如果传递普通变量的地址,形参用指针;传递指针的地址,形参用二级指针

空指针

在C++和C中,用0或NULL都可以表示空指针。

声明指针后,在赋值之前,让它指向空,表示没有任何地址。

1)使用空指针的后果

如果对空指针解引用,程序会崩溃。

如果对空指针使用delete运算符,系统忽略该操作,不会出现异常。所以内存释放后,也应该把指针指向空。

为什么空指针访问会出现异常?

NULL指针分配的分区:其范围是从 0x000000000x0000FFFF。这段代码是空闲的,对于空闲的空间而言,没有相对应的物理存储器,与之相对应,所以对这段代码来说,任何读写操作都会引起异常的。空指针是程序无论在核实都没有物理存储器与之相对应的地址。为了保障“无论何时”这个条件,需要人为划分一个空指针的区域,固有上面NULL指针分区。

2)C++ 11的nullptr

用0和NULL表示空指针会产生歧义,C++11建议用 nullptr表示空指针,也就是 (void *)0

NULL在C++中就是0,这是因为在C++中 void * 类型是不允许隐式转换成其他类型的,所以之前C++中用0来代表空指针,但是在重载整型的情况下,会出现上述的问题。所以C++11加入了nullptr,可以保证在任何情况下都代表空指针,而不会出现上述的情况,因此,建议用nullptr替代NULL,而NULL就当做0使用。

注意:在Linux平台下,如果使用nullptr,编译要加 -std=c++11参数

野指针

野指针就是指针指向的不是一个有效(合法)的地址。

在程序中,如果访问野指针,可能会造成程序的崩溃。

出现野指针的情况主要有三种:

  • 1)指针在定义的时候,如果没有初始化,它的值是不确定的(乱指一气)。
  • 2)如果指针指向了动态分配的内存,内存被释放后,指针不会置空,但是,指向的指针已失效。
  • 3)指针指向的变量超越变量的作用域(变量的内存空间已被回收),作用域指的是是局部变量还是全局变量。

规避方法:

  • 1)指针在定义的时候,如果没地方指,就初始化为nullptr。

  • 2)动态分配的内存释放后,将其置位nullptr。

  • 3)函数不要返回局部变量的地址。

    注意:野指针的危害比空指针要大很多,如果访问野指针,可能会造成程序的崩溃。是可能,表示一定,程序的表现是不稳定,增加了调试的难度。

一维数组与指针

1) 指针的算术

  • 将一个整型变量加1后,其值将增加1。
  • 但是,将指针变量(地址的值)加1后,增加的量等于它指向的数据类型的字节数。

2) 数组的地址

  • a) 数组在内存中占用的空间是连续的。
  • b) C++数组名解释为数组的第0个元素的地址。
  • c) 数组第0个元素的地址和数组首地址的取值是相同的。
  • d) 数组第n个元素的地址是:数组首地址+n
  • e) C++编译器把 数组名[下标]解释为 *(数组首地址+下标)

3) 数组的本质

  • 数组是占用连续空间的一块内存,数组名被解释为数组第0个元素的地址。C++操作内存有两种方法:数组解释法和指针表示法,他们说等价的。

4)数组名不一定会被解释为地址

  • 在多数情况下,C++将数组名解释成第0个元素的地址,但是,将sizeof运算符用于数组名,返回的是整个数组的内存空间的字节数。
  • 可以修改指针的值,但数组是常量,不可修改。

一维数组用于函数的参数

1)指针的数组表示

  • 在C++内部,用指针来处理数组。
  • C++编译器把 数组名[下标]解释为 *(数组首地址+下标)
  • C++编译器把 地址[下标]解释为 *(地址+下标)

2)一维数组用于函数的参数

  • 一维数组用于函数的参数时,只能传数组的地址,并且必须把数组长度也传进去,除非数组中有最后一个元素的标志。

  • 书写的方法有两种:

    void func(int* arr,int len)

        void func(int* arr[],int len)

  • 注意:

    在函数中,可以用数组表示法,也可以用指针表示法。

    在函数中,不要对指针名用sizeof运算符,由于64位操作系统指针大小是永远是8,并不是数组占用内存的大小。

用 new 动态创建一维数组

普通数组在栈上分配内存,栈很小;如果需要存放更多的元素,必须在堆上分配内存。

动态创建一维数组的语法:数据类型 *指针=new 数据类型[数组长度]

释放一维数组的语法:delete []指针;

注意:

  • 动态创建的数组没有数组名,不能用 sizeof 运算符。
  • 可以用数组比色法和指针表示法两种方式使用动态创建的数组。
  • 必须使用delete[]来释放内存(不能只用 delete)。
  • 不用用delete[]来释放不是new[]分配的内存。
  • 不要用delete[]释放同一个内存块两次(否则等同于操作野指针)。
  • 对空指针用delete[]是安全的(释放内存后,应该把指针置空nullptr)。
  • 声明普通数组的时候,数组长度可以用变量,相当于在栈上动态创建数组,并且不需要释放。
  • 如果内存不足,调用new会产生异常,导致程序终止;如果在new关键字后面加(std::nothrow)选项,则返回nullptr,不会产生异常。
  • 为什么用delete[]释放数组的时候,不需要指定数组的大小?因为系统会自动跟踪分配的内存。

二维数组用于函数的参数

int* p;      //整型指针。
int* p[3];   //一维整型指针数组,元素是3个整型指针(p[0]、p[1]、p[2])。
int* p();    //函数p的返回值类型是整型的地址。
int (*p)(int, int);    //p是函数指针,指针指向的函数具有两个int型的形参。函数的返回值是整型。

1)行指针(数组指针)

声明行指针的语法:数据类型 (*行指针名)[行的大小]; //行的大小即数组长度。

//行指针的举例
int(*p1)[3]; //p1是行指针,用于指向数组长度为3的int型数组。
int(*p2)[5]; //p2是行指针,用于指向数组长度为5的int型数组。
double(*p3)[5]; //p3是行指针,用于指向数组长度为5的double型数组。

一维数组名被解释为数组的第0个元素地址。

对一维数组名取地址得到的是数组的地址,是行地址。

2)二维数组名是行地址

int bh[2][3] = {{11,12,13},{21,22,23}};

bh 是二维数组名,该数组有2元素,每一个元素又是一个数组长度为3的整型数组

bh被解释为数组长度为三的整型数组类型的地址。

如果存放bh的值,要用数组长度为3的整型数组类型的指针。

int (*p)[3] = bh

3)把二维数组传递给函数

如果要把bh传给函数,函数的声明如下:

void func(int (*p)[3],int len);

void func(int p[][3],int len);


文章作者: ZhiQ
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 ZhiQ !
  目录