C++指针的理解

http://zeus.softweek.net/item-slt-1.html
我们知道,在计算机内存中可以通过变量名称作为标识符访问变量。这种方法,程序不需要关心数据在内存的物理地址;无论何时涉及变量时,它简单地使用标示符。
对于C++程序,计算机的内存就像一系列内存单元的组合,这些单元的大小是1个字节,具有独立的地址。这些单字节内存单元被按照一定的方式组织起来,允许表述更大的数据单元,它们具有连续的地址。
基于此,使用每一个单元的独立地址可以很容易地访问内存。例如,地址为1776的内存单元,跟随在地址1775,在地址1777之前。
声明变量时,内存需要分配一个固定的地址存储这个变量。通常,C++程序不会立即决定变量被保存的真实内存地址。幸运的是,这个任务被留给了程序运行的环境里,通常,操作系统运行时决定详细的内存位置。但是,在运行时间内,程序能够获取变量的内存地址是非常有用的。

1、取址操作符(&)

变量地址的获取可以通过&-取址操作符实现:

foo = &myvar;

在运行之前,内存中变量的真实地址是无法得知的,但是,为了帮助我们理解一些概念,我们假设,myvar变量在运行时,内存中的地址是1776。
在这种情况下,考虑下面的代码片段:

myvar = 25;
foo = &myvar;
bar = myvar;

这段代码执行后,每个变量的值如下面的框图所示:
你好
首先,我们把值25赋给变量myvar,第二条语句把myvar的地址赋给变量foo,第三条语句把myvar的值赋给变量bar。变量存储着另一个变量的地址,这个变量被称为指针。指针在底层编程语言中是一个非常有用的特点。稍后,我们将会看到怎样声明和使用指针。

2、解引用操作符 (*)

指针一个比较有意思的特性就是可以直接访问它所指向的变量。方法就是在指针名称前面加上解引用操作符(*)。例如:

baz = *foo;

这可以被读作“baz等于foo指向的值”。baz将会被赋值为25,因为foo = 1776,所以在地址1776处的值为25。
这里写图片描述

baz = foo;   // baz equal to foo (1776)
baz = *foo;  // baz equal to value pointed to by foo (25)

(1). & 是取址操作符
(2). *是解引用操作符
因为它们可以被看做相反的操作运算符。早些时候,我们经常使用下面的赋值语句:

myvar = 25;
foo = &myvar;

那么,下面的这些表达式也都是正确的。

myvar == 25
&myvar == 1776
foo == 1776
*foo == 25

这几条语句都十分的简单,就不一一详述了。
基于此,我们可以推断,只要foo这个指针没有被改变,下面的表达式就是正确的。

*foo == myvar

3、声明指针

因为指针具有直接引用它所指向的值得能力,所以,指针所指向内容的类型(例如,char,int,float)不同会有不同的特性。一旦解引用发生,必须要知道数据类型。所以,指针在声明的时候需要指明它所要指向的数据类型。
声明指针的语法如下所示:

type *name;

不同的数据类型,声明不同的指针。

int *number;
char *character;
double *decimals;

虽然,它们是指向不同数据类型的指针,但是这些指针的大小是相同的。它们在内存中的大小依赖于程序运行的平台。而它们指向的数据值在内存中的占用大小是不同的。
注意:在这里(*)仅仅说明后面的变量是一个指针。
让我们看一个指针的例子:

// my first pointer
#include <iostream>
using namespace std;

int main ()
{
  int firstvalue, secondvalue;
  int *mypointer;

  mypointer = &firstvalue;
  *mypointer = 10;
  mypointer = &secondvalue;
  *mypointer = 20;
  cout << "firstvalue is " << firstvalue << '\n';
  cout << "secondvalue is " << secondvalue << '\n';
  return 0;
}

我们注意到,尽管firstvalue或secondvalue都没有被直接赋值,但是,通过指针mypointer间接地赋给了一个值。步骤如下:
首先,取址符号&取得firstvalue变量的地址赋给mypointer;然后,把10赋给mypointer所指向的值。因为,此时mypointer指向的是firstvalue在内存中的位置,所以事实上,修改了firstvalue的值。
为了证明指针在生命周期内是可以指向不同变量的,使它指向了secondvalue这个变量,处理流程与上面相同。
这儿是更为复杂的例子:

// 更多的指针
#include <iostream>
using namespace std;

int main ()
{
  int firstvalue = 5, secondvalue = 15;
  int *p1, *p2;

  p1 = &firstvalue;  // p1 = firstvalue变量地址
  p2 = &secondvalue; // p2 = firstvalue变量地址
  *p1 = 10;          // 修改firstvalue的值
  *p2 = *p1;         // 修改secondvalue的值
  p1 = p2;           // p1 = p2 只是复制指针
  *p1 = 20;          // p1指向的内容为20;

  cout << "firstvalue is " << firstvalue << '\n';
  cout << "secondvalue is " << secondvalue << '\n';
  return 0;
}

运行结果:

firstvalue is 10
secondvalue is 20

后面的注释对程序的执行过程做了详尽的说明,不再阐述。

int *p1, *p2

注意上面这行代码,星号(*)不能省略。千万不能写成:

int * p1, p2;

4、指针和数组

指针和数组有着极大的相似性,尤其是对数组第一个元素的操作。数组有时候也会被转化成正确类型的指针,看下面的代码:

int myarray [20];
int * mypointer;
mypointer = myarray;

上面的代码执行完之后,mypointer和myarray是等价的,且具有相似的属性。但是它们之间还是有不同的,最主要的差异就是mypointer被赋予不同的地址,而myarray绝对不能被赋予任何东西。myarray总是代表着20个类型为int的内存块。因而,下面的赋值是不合法的:

myarray = mypointer;

让我们看下面的例子:

// more pointers
#include <iostream>
using namespace std;

int main ()
{
  int numbers[5];
  int *p;
  p = numbers;  *p = 10;
  p++;  *p = 20;
  p = &numbers[2];  *p = 30;
  p = numbers + 3;  *p = 40;
  p = numbers;  *(p+4) = 50;
  for (int n=0; n<5; n++)
    cout << numbers[n] << ", ";
  return 0;
}

执行结果是:

10, 20, 30, 40, 50, 

指针和数组支持相同的操作符号集,也具有相同的意思。主要的差别还是指针能够被赋予新的地址,但是数组名不能。
在关于数组和[]的章节里还会专门的讲解。这些中括号被看成偏移操作符。例如:

a[5] = 0;       // a [offset of 5] = 0
*(a+5) = 0;     // pointed to by (a+5) = 0 

这两个表达式是相等的且合法的,a不仅仅是指针还是一个数组。记住,数组名可以看做一个指针,这个指针指向它的第一个元素。

5、指针的初始化

指针在程序里应该被赋值,指向某一个具体的地址,就像下面的定义:

int myvar;
int *myptr = &myvar;

这段代码与下面这段是等效的:

int myvar;
int *myptr;
myptr = &myvar;

当指针被初始化的时候,应该被初始化它们所指向值得地址,而不应该是它们指向的值。因此,下面的代码是不合理的:

int myvar;
int *myptr;
*myptr = &myvar;

指针可以被初始化值的地址,也可以是另一个指针的值。

int myvar;
int *foo = &myvar;
int *bar = foo;

6、指针运算

作用于指针上的运算符操作和作用于常规的整形上的操作是有一点不同的。首先,只有加减操作被允许;其余的对于指针来说没有意义。但是,指针的加减操作根据所指向数据类型的不同,而表现出不同的行为。
在介绍数据基本类型的时候,知道它们有不同的大小。例如:char型具有一个字节的大小,short型通常会比char型大,int和long型更大;具体值大小可能依赖于系统。这里,我们假设,char 为1字节,short占用2个字节,long占用4个字节。
Suppose now that we define three pointers in this compiler:
假设,我们在编译器里定义三个指针:

char *mychar;
short *myshort;
long *mylong;

假设它们分别指向三个内存地址1000,2000,3000。
因此,如果对它们进行下面的操作:

++mychar;
++myshort;
++mylong;

上面这段代码执行结果就是mychar = 1001,myshort=2002,mylong = 3004;从中我们可以看出,指针的加操作就是加上所指向数据类型所占用字节数量。
这里写图片描述

上面的图片就是上面这段代码的执行过程。
指针的操作,后缀的操作符比前缀的操作符具有更高的优先级。因此,下面的表达式:

 *p++

p++是等价于(p++)的。它所做的也是p+1,所以它指向下一个元素。但是因为++是被用作后缀的,整个表达式的值等于指针初始指向的值,(也就是说,使用指针自加之前的值)。
下面我们来看四种组合:

*p++    // same as *(p++): increment pointer, 
        //and dereference unincremented address
*++p    // same as *(++p): increment pointer, 
        //and dereference incremented address
++*p    // same as ++(*p): dereference pointer, 
        // and increment the value it points to
(*p)++  // dereference pointer, and post-increment
        //the value it points to

这种操作一个典型的应用如下面所示:

*p++ = *q++;

因为++具有更高的优先级,所以结果就是先把*q的值赋给*p,然后p和q实现自加。等效过程如下面所示:

*p = *q;
++p;
++q;

通常,加上括号减少困惑,增加程序的易读性。

7、指针和const关键字

可以通过指针访问它所指向的值,并修改这个值;但是也有可能声明指针仅仅是指向这个值,但是不修改这个值。要想实现这个目的就可以使用const。

int x;
int y = 10;
const int *p = &y;
x = *p;          // ok: 读取 p
*p = x;          // error: 修改 p, 这是非法的

在这里,p指向一个变量,但是这个变量是用const关键词限定的,它是不能被修改的。另外需要注意的是,&y取得的是一个整形指针,但是它却被赋给了一个常整形的指针。这是允许的:这里会实现隐式转换。但是,反过来却是不被允许的。这是一个数据安全性问题,指向常量的指针是不能隐式转换成非常数类型的指针的。
作为指向常量的指针使用情况中的一种是,作为函数参数:一个函数可以使用非常量指针作为参数进行传递,修改所指向的值;另一种是使用常量指针作为参数,这个参数是不能被修改的。

// pointers as arguments:
#include <iostream>
using namespace std;

void increment_all(int *start, int *stop)
{
  int *current = start;
  while (current != stop) {
    ++(*current);  // increment value pointed
    ++current;     // increment pointer
  }
}

void print_all (const int *start, const int *stop)
{
  const int *current = start;
  while (current != stop) {
    cout << *current << '\n';
    ++current;     // increment pointer
  }
}

int main ()
{
  int numbers[] = {10,20,30};
  increment_all(numbers,numbers+3);
  print_all(numbers,numbers+3);
  return 0;
}

函数print_all()中,const只是限定的指针指向的内容,所以指针仍然是可以修改的。而它们所指向的值是不能被修改的。
当然了const也可以用来修饰指针。这要看const修饰的位置,参见下面的代码:

int x;
      int *       p1 = &x;  // 指向非常数的非常数指针
const int *       p2 = &x;  // 非常数指针指向常数
      int * const p3 = &x;  // 常数指针指向非常数
const int * const p4 = &x;  // 常数指针指向常数

const和指针的语法是非常有技巧的,找到最合适的使用往往需要一些经验。但是无须担心,我们接下来的章节中还会展示更多这样的例子。

const限定符放在类型符后边或者前边,效果是一样的:

const int * p2a = &x;  // 非常数指针指向常数
int const * p2b = &x;  // 也是非常数指针指向常数

推荐使用const限定符放在类型符前边。

8、指针和字符串

在指针之前,字符串是包含无结束的字符序列的数组。
字符串就是包含所有字符+终止字符的数组。例如:

const char *foo = "hello";

假设字符串的首地址是1702,我们能够看到字符串在内存中的存储分布如下:
这里写图片描述
注意foo是一个指针,且包含值1702,而不是’h’,也不是’hello’,尽管1702却是它们的地址。

指针foo指向一个字符串序列。且因为指针和数组在某种程度上表现出的行为相同,指针可以用来访问字符串数组。例如:

*(foo+4)
foo[4]

上面的表达式都是字符串数组中的成员’o‘。

9、指向指针的指针

当然了,C++也允许指针指向指针的指针。

char a;
char * b;
char **c;
a = 'z';
b = &a;
c = &b;

这里假设为变量7230,8092和10502随机选择内存位置,表现如下:
这里写图片描述
c 是 char**,值等于8092
c 是char ,值等于 7230
**c是char,值为’z’
10、void指针
C++中,void代表着没有指定类型。因此,void指针是指向那些没有类型值的指针。(因而具有不定的长度和不定的引用属性)。
这就给了void指针很大的灵活性,能够指向任何数据类型,从整形到浮点型,再到字符串。作为交换,他们也有很大的限制:通过他们指向的数据不能直接被引用。基于这个原因,void 类型指针需要被转换成其它类型的指针,指向一个确定数据类型的地址。
它最可能的使用情况就是给函数传递通用型参数。例如:

// increaser
#include <iostream>
using namespace std;

void increase (void* data, int psize)
{
  if ( psize == sizeof(char) )
  { char *pchar; pchar=(char *)data; ++(*pchar); }
  else if (psize == sizeof(int) )
  { int *pint; pint=(int *)data; ++(*pint); }
}

int main ()
{
  char a = 'x';
  int b = 1602;
  increase (&a,sizeof(a));
  increase (&b,sizeof(b));
  cout << a << ", " << b << '\n';
  return 0;
}

sizeof 是C++语言内集成的一个操作符,返回它的参数的字节数大小。对于非动态数据类型,这个值是一个常数,因此sizeof(char) = 1,因为char总是占用一个字节。

11、非法指针和null指针

直接看下面的例子:

int *p;               // 未初始化的指针 (局部变量)

int myarray[10];
int *q = myarray+20;  // 访问越界 

上面的代码中,p和q都没有指向包含已知值地址,但是上面的声明不会发生任何错误。在C++中,指针允许指向任何地址值。使用这样的地址指针会造成错误。使用这样的地址指针访问地址,会造成没有定义的行为。可能会发生错误,也可能会访问随机值。
但是,有时候,指针确实需要指向一个空地方,这儿不是非法地址。对于这种情况,可以使用null指针。在C++中,有两种赋值方式:0的整型值,或者nullptr关键字

int *p = 0;
int *q = nullptr;

这儿,p和q都是null指针,意味着他们都明确地指向了空地方,在旧代码中使用常数constant NULL。

int *r = NULL;

NULL被定义在标准库的几个头文件里。
不要混淆null指针和void指针!!null指针是代表指针指向没有地方,而void指针是指向的地址里存储类型不确定。

12、函数指针

C++允许指针指向函数。例如,把一个函数作为参数传递给另一个函数。函数指针的声明和正常的函数声明具有相同的语法,除了函数的名称是用()包括,星号*被插入到名字前面:

// pointer to functions
#include <iostream>
using namespace std;

int addition (int a, int b)
{ return (a+b); }

int subtraction (int a, int b)
{ return (a-b); }

int operation(int x, int y, int (*functocall)(int,int))
{
  int g;
  g = (*functocall)(x,y);
  return (g);
}

int main ()
{
  int m,n;
  int (*minus)(int,int) = subtraction;

  m = operation (7, 5, addition);
  n = operation (20, m, minus);
  cout <<n;
  return 0;
}

在上面的例子里,minus被声明为一个具有两个整形参数的函数指针。它直接被初始化指向函数subtraction:

int (* minus)(int,int) = subtraction;
  • 11
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值