C++数组(二维数组)与指针 以及 函数传递/返回指针和数组参数

    • 概论
    • 指针基础
    • 指针进阶
    • 一维数组的定义与初始化
    • 指针和数组
    • 指针运算
    • 多维数组和指针
    • 指针形参
    • 数组形参
    • 返回指针和数组
    • 结语

本文参考资料 C++ Primer, 5e; Coursera北大数据结构与算法课程。

1. 概论

很全面的指针在C\C++语言中是很重要的内容,并且和指针有关的内容一向令人头大。在本教程中,我总结了一些关于指针和数组的用法(尤其是指针和二维数组)。大部分关于指针和数组的问题应该可以再本文找到答案。

2.指针基础

指针是指向另外一种类型的复合类型。
指针本身就是一个对象,允许对指针进行赋值和拷贝;指针无需在定义时赋初值。

指针定义
“&”是取地址操作符。

int num=1;
int *p=# //(&是取地址操作符)

利用指针访问对象
使用解引用操作符“*”。

cout<<*p<

输出结果为1。

指针的状态

  • 指向一个对象
  • 指向紧邻对象所占空间的下一个位置
  • 空指针 int *p=nullptr;
  • 无效指针

指针作为条件判断参数
例如:

if(p){}

只要指针p不是0,那么条件就为真。
另外值得注意的是,对于两个类型相同的指针,可以用“==”或者“!=”来比较。若两个指针存放的地址相同,则它们相等,否则不等。

3. 指针进阶

指向指针的指针
由于指针是对象,所以指针也有自己的地址。因此,C++语言允许把一个指针指向另一个指针。
例子:

int i=9;
int *p1=&i;
int **p2=&p1;
cout<

结果是打印3个9。

指针与const限定符
这里有2个初学者容易混淆的概念,及指向常量的指针常量指针。根据其英文名字可能比较容易记住:
指向常量的指针(pointer to const)是说这个指针是一个普通的指针,它指向了一个常量,如果你愿意,它也可以指向其他对象,并且可以令一个指向常量的指针指向另一个非常量;

const double pi=3.1415;
double *p1=π//error for p1 is a general pointer
const double *p2=π//correct
*p2=6.28;//error for pi is a const variable and p2 is const

常量指针(const pointer)是说这个指针本身就是一个常量对象,所以它不能指向其他对象,但是不意味着它不能改变所指向对象的值。

int num=9;
int *const p1=#//correct, but remember that p1 cannot point to other objects
*p1=18;//correct. You can use the const pointer to change the value of a unconst variable
const double e=2.71;
const double *const p2=&e;//p2 is a const pointer points to a const object

4. 一维数组的定义与初始化

定义

int arr[10];//含有10个整型的数组
int *arr2[3];//含有3个整型指针的数组

一般情况下,数组的元素被默认初始化。

显示初始化

int arr[]={1,2,3};
int arr2[4]={1,2,3,4};

可以用字符串字面值初始化字符数组,但是需要记得字符串字面值结尾有一个空字符

char arr[5]={'h','e','l','l','o'};//correct
char arr[5]="hello";//error, initilizer-string for the chars array is too long

访问数组元素
使用下标访问数组元素,注意数组的下标从0开始
C++ 11标准增加了 range for语句可以遍历数组元素:

for(auto i : arr)//auto用来自动确定类型
{
    cout<

使用range for的好处在于不用担心数组越界。

5. 指针和数组

可以用一个指针指向数组元素:

int arr[]={1,2,3,4,5,6,7,8,9,0};
int *p=&arr[0];//此时p是一个指向数组首元素的指针

数组有一个特性,很多用到数组的地方,编译器会自动把数组名替换为一个指向数组首元素的指针

int arr[]={1,2,3,4,5,6,7,8,9,0};
int *p=&arr[0];//此时p是一个指向数组首元素的指针
cout<//result 0x69fee4
cout<//result 0x69fee4

注意:由于数组名arr是一个常量,因此* arr++是没有意义的,并且编译器会报错,因为arr++试图修改arr的值。但是(arr+2)是有意义的,因为这并没有试图修改arr的值。同理,如果令*p=&arr[0],那么我们也是可以使用*p++的,因为p不是常量。

标准库函数begin & end
尽管可以得到尾后指针,但这种做法极易出错。C++11标准引入了两个名为begin和end的函数:

int a[]={1,2,3,4,5,6};
int *beg=begin(a);//pointer to the first element
int *last=end(a);//pointer to the position next to the last element

//output the elements of the array
while(beg!=end)
{
    cout<<*beg<

6. 指针运算

给一个指针加上(减去)某个整数N,结果依然是指针。新指针与原来的指针相比前进或者后退了N个位置。
两个指针相减的结果是它们之间的距离。
为了更好的理解之歌问题,举如下例子:

#include 
using namespace std;

int main()
{
    int a[3][3]={{6,1,7},
    {2,5,4},
    {8,3,9}
    };

    cout<//1
    cout<1<//2
    cout<<&a+1<//3
    cout<<*a<//4
    cout<<*a+1<//5

    return 0;
}

程序结果(你的结果可能会有所不同)
0x69fec0
0x69fecc
0x69fee4
0x69fec0
0x69fec4

第一个打印结果为0x69fec0,给a+1后,结果为0x69fecc,变大了12。为什么会变大12呢?要知道每个整数都是4个字节,为什么不是变大4呢?答案是:由于a是一个二维数组,所以a指向的第一个元素是一个含有3个整数的数组,因而加1后是指向下一个子数组,所以地址的值会变大12.

同理,&a是指向一个二维数组,因此加1后地址值会变大0x24=36。

而*a指向第一个子数组的第一个元素——这是一个整数,因此加1后地址值变大了4。同时你可以发现a和*a的地址是一样的,这是因为第一个子数组和第一个整型元素的起始地址是一样的。

7. 多维数组和指针

严格来说,C++中是没有多维数组的,通常所说的多维数组其实是数组的数组。
在本文第6部分的例子中展示了二维数组的初始化方法,更高维的数组初始化方法是类似的。这里再次详细说明一下数组名和指针的关系。以本文第6部分的例子为例:

a是指向二维数组第一个元素即第一个子数组的指针,等价于&a[0];
a[0]是指向a[0][0]的指针,等价于*a;
a[0][0]指向第一个整型元素‘6’, 等价于**a;
&a指向整个二维数组;
总之,加*会将指针降一级,&会把指针升一级。

下标访问
多维数组同样可以下标访问,例如a[0][0]的值是6.

8. 指针形参

当使用指针作为函数参数的时候,执行的是指针拷贝的操作,拷贝的是指针的值。拷贝之后两个指针是不同的指针,但是它们所指向的对象是一样的,因此可以通过操作指针来改变指针所指向对象的值。

void change(int *p)
{
    *p=32;
}

限制指针的功能
很多情况下我们使用指针是为了避免拷贝对象,但是并不希望更改对象的值。这种情况下,使用const限定符限制指针的功能是一个不错的选择。

void test(const int *p)
{
    ...
    ...
}

9. 数组形参

由于数组不允许拷贝,因此我们无法以传值的方式传递一个数组;因为数组名相当于数组第一个元素的指针,因此可以通过传递指针的形式来在函数中操作数组。
一以下3个声明是等价的:

void print(const int *);
void print(const int[]);
void print(const int[5]);

由于数组是以指针形式传递给函数的,因此一开始的时候函数并不知道数组的维度。因此有时候有必要显示传递一个维度参数。
当函数不需要对数组元素进行写操作的时候,数组形参最好用const限定符限制指针功能,详见本文第8部分示例。

传递多维数组
所谓多维数组其实是数组的数组。和一维数组一样,我们实际传递的是数组的指针。下面2个声明是等价的:

void print(int (*p)[3],int rowsize){...}
void print(int (p[][3],int rowsize){...}

这样的例子可能没有什么直观的感受,下面我们用一个详细的例子来说明。

#include 

using namespace std;
void print1(int (*p)[3])//注意*p两边的括号不可缺少。
{
    cout<1][1]<void print2(int p[][3])
{
    cout<0][0]<int main()
{
    int a[2][3]={{1,2},{3,4}};
    print1(a);
    print2(a);
    return 0;
}

注意,print1(int (*p)[3])函数里面,形参*p两边的括号必不可少。
*p[3]表示3个指针构成的数组
(*p)[3]表示指向含有3个整数数组的指针。

结果
4
1

你可能对这种方法并不是很满意,为什么呢?因为使用这种方式必须显示指定第二维的维度,而有些时候这个维度是无法获得的。比如有以下题目:

摘抄自:北大poj
描述
在一个m×n的山地上,已知每个地块的平均高程,请求出所有山顶所在的地块(所谓山顶,就是其地块平均高程不比其上下左右相邻的四个地块每个地块的平均高程小的地方)。
输入
第一行是两个整数,表示山地的长m(5≤m≤20)和宽n(5≤n≤20)。
其后m行为一个m×n的整数矩阵,表示每个地块的平均高程。每行的整数间用一个空格分隔。
输出
输出所有上顶所在地块的位置。每行一个。按先m值从小到大,再n值从小到大的顺序输出。

样例输入
10 5
0 76 81 34 66
1 13 58 4 40
5 24 17 6 65
13 13 76 3 20
8 36 12 60 37
42 53 87 10 65
42 25 47 41 33
71 69 94 24 12
92 11 71 3 82
91 90 20 95 44
样例输出
0 2
0 4
2 1
2 4
3 0
3 2
4 3
5 2
5 4
7 2
8 0
8 4
9 3

如果题目要求你必须写一个函数来处理,是不是感觉之前讲解的参数传递方法就不实用了呢?因为你也不知道一开始第二个维度是多少啊!
其实万变不离其宗,我们只要想办法传入一个地址,就可以通过这个地址访问到这个数组的所有元素。这里最重要的就是要搞清楚本文第6部分和第7部分讲解的关于指针运算的问题,这对于初学者可能会感觉有点乱,但是只要慢慢去想,你就会发现这一切都是那么的自然。

为了解决这个问题,首先看看如何访问整个二维数组。
原则上只要传入了第一个元素的指针,我们就可以通过对这个指针进行运算从而遍历整个数组。

void print (int *a,int m,int n)
{
    for(int i=0;i!=m;++i)
    {
        for(int j=0;j!=n;++j)
            cout<<*(a+i*n+j);
        cout<

假设有一个5*5的二维数组a。这个函数,我们可以传入print(*a,5,5)。还记得吗?在第7部分我们说过*a就是指向a[0][0]的指针。这样就可以打印整个数组了。我记得我刚学习这里的时候总是纠结于,我是不是在定义函数的时候形参是不是应该是print(int**p, int m, int n)呢?其实是没有必要的,因为二维数组名并不是指向指针的指针,你需要的只是一个入口而已。
下面给出我的程序:

#include 
using namespace std;
static int m,n;
void hill(int **a)
{
    for(int i=0;i!=m;++i)
    {
        for(int j=0;j!=n;++j)
        {
            int num=*((int*)a+i*n+j);
            if(i==0)
            {
               if(j==0)
               {
                   if(num>=*((int*)a+i*n+j+1) && num>=*((int*)a+(i+1)*n+j))
                   {
                       cout<" "<else if(j==n-1)
               {
                   if(num>=*((int*)a+i*n+j-1) && num>=*((int*)a+(i+1)*n+j))
                   {
                       cout<" "<else
               {
                    if(num>=*((int*)a+i*n+j-1) && num>=*((int*)a+i*n+j+1) && num>=*((int*)a+(i+1)*n+j))
                    {
                        cout<" "<else if(i==m-1)
            {
                if(j==0)
               {
                    if(num>=*((int*)a+(i-1)*n+j) && num>=*((int*)a+i*n+j+1))
                    {
                        cout<" "<else if(j==n-1)
               {
                   if(num>=*((int*)a+(i-1)*n+j) && num>=*((int*)a+i*n+j-1))
                   {
                       cout<" "<else
               {
                    if(num>=*((int*)a+(i-1)*n+j) && num>=*((int*)a+i*n+j-1) && num>=*((int*)a+i*n+j+1))
                    {
                        cout<" "<else
            {
               if(j==0)
               {
                   if(num>=*((int*)a+(i-1)*n+j) && num>=*((int*)a+(i+1)*n+j) && num>=*((int*)a+i*n+j+1))
                   {
                       cout<" "<else if(j==n-1)
               {
                    if(num>=*((int*)a+i*n+j-1) && num>=*((int*)a+(i-1)*n+j) && num>=*((int*)a+(i+1)*n+j))
                        cout<" "<else
               {
                    if(num>=*((int*)a+i*n+j-1) && num>=*((int*)a+i*n+j+1) && num>=*((int*)a+(i-1)*n+j) && num>=*((int*)a+(i+1)*n+j))
                        cout<" "<int main()
{

    cin>>m>>n;
    int a[m][n];
    for(int i=0;i!=m;++i)
    {
        for(int j=0;j!=n;++j)
        {
            cin>>a[i][j];
        }
    }
    //int *p=*a;
    hill((int**)a);

    return 0;
}

10. 返回指针和数组

返回指针

#include
using namespace std;
int a[]={11,21,31,41};
int *f()
{
    return a;
}

int main()
{
    cout<<*f()<//result is 11
}

上面的例子展示了如何返回一个指针。或许吧函数定义写成如下形式更好理解。

int* f(){... ...}

这种形式展示了函数 f 的返回类型是指针而不是让人误以为函数名是 *f

永远不要试图返回局部对象的指针。因为局部变量(对象)的生命周期在函数调用结束后会消失,此时你返回的地址的内容可能已经发生了变化。
如果需要返回局部变量的指针,你需要把这个变量声明为static

返回数组
由于指针不能拷贝,因此函数不能返回数组,但是可以返回数组的指针或者引用。其实上面的例子就是一个返回数组的例子,故不再叙述。

11. 结语

本文主要讲述了指针和数组的用法。我觉得这对于初学者还是一个比较全面的教程,希望大家喜欢。

你可能感兴趣的:(C++)