C/C++ 数组形参


一维数组

数组形参会被弱化为指针,所以处理数组的函数通常通过指向数组中元素的指针来处理数组。

// three equivalent definitions of printValues
void printValues(int *) { /* … */ }
void printValues(int []) { /* … */ }
void printValues(int [10]) { /* … */ }

上面3种定义等价,形参类型都是int *,通常使用第1种。第2种形式虽然看起来更直观,但容易引起误解,因为函数操纵的毕竟不是数组本身,而是指向数组元素的指针。第3种形式的数组长度是被忽略的,这里的10并无实际的约束作用,在printValues内部不应依赖这个数组长度做事情。

和其他类型一样,形参也可以是数组的引用。这时编译器不会将数组实参转换为指针,而是传递数组的引用本身。数组大小成为形参和实参类型的一部分。编译器检查数组实参的大小与形参的大小是否匹配。

// ok: parameter is a reference to an array; size of array is fixed
void printValues(int (&arr)[10])
{
    for (size_t i = 0; i != 10; ++i)
        cout << arr[i] << endl;
}

int main()
{
    int i = 0, j[2] = {0, 1};
    int k[10] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};

    printValues(&i); // error: argument is not an array of 10 ints
    printValues(j); // error: argument is not an array of 10 ints
    printValues(k); // ok: argument is an array of 10 ints

    return 0;
}

这个版本的printValues只严格接受含有10个ints的数组,所以在printValues函数体内依赖数组大小也是安全的。&arr两边的括号是必须的,否则int &arr[10],会被当做是含有10个引用的数组。

也可以传递多维数组,所谓多维数组实际就是数组的数组。多维数组同样以指向首元素的指针方式传递。除了第一维以外的所有维的长度都必须明确指定。

// first parameter is an array whose elements are arrays of 10 ints
void printValues(int (*matrix)[10], int rowSize);
void printValues(int matrix[][10], int rowSize);实际上形参还是被弱化为一个指针,只不过它现在所指向的是含有10个ints的数组。

因为非引用型数组形参被弱化为指针,所以无法得知数组的大小,这就容易造成数组越界。一般处理方法有以下三种。

(1)在数组尾端放置一个结束标记,类似于c-style字符串的\0结束符。

(2)传递指向数组的第一个和最后一个元素的下一位置的指针,c++标准库程序采用这种方法。

void printValues(const int *beg, const int *end)
{
    while (beg != end)
        cout << *beg++ << endl;
}(3)显示传递表示数组大小的形参

void printValues(const int ia[], size_t size)
{
    for (size_t i = 0; i != size; ++i)
        cout << ia[i] << endl;
}


二维数组

一道面试题引发的问题,首先要知道[]的优先级高于*,题目:

char **p,a[6][8]; 问p=a是否会导致程序在以后出现问题?为什么?

直接用程序说明:

#include<stdio.h>

void main()
{
    char **p,a[6][8];
    p = a;
    printf("\n");
}

编译,然后就会发现通不过,报错:错误 1 error C2440: “=”: 无法从“char [6][8]”转换为“char **”

于是乎,我看了下《C专家编程》里10.5节—使用指针向函数传递一个多维数组。

方法一,函数是 void fun(int arr[2][3]); 这种方法只能处理2行3列的int型数组。

方法二,可以省略第一维的长度。函数是 void fun(int arr[][3]);这种方式虽然限制宽松了一些,但是还是只能处理每行是3个整数长度的数组。或者写成这种形式 void fun(int (*arr)[3]);这是一个数组指针或者叫行指针,arr和*先结合使得arr成为一个指针,这个指针指向具有3个int类型数据的数组。

方法三,创建一个一维数组,数组中的元素是指向其他东西的指针,也即二级指针。函数是 int fun(int **arr);这种方法可以动态处理各行各列不一样长度的数据。

注意:只有把二维数组改成一个指向向量的指针数组的前提下才可以这么做!比如下面的程序可以正常输出abc:

#include <iostream>
using namespace std;
void test(char **ptr)
{
    cout << *ptr << endl;
}
 
int main()
{
    char *p[3] = {"abc", "def", "ghi"};
    test(p);
    return 0;
}

在《C专家编程》10.3节的小启发里讲的很透彻:(以下这段文字及对比一定要认真分析!)

数组和指针参数是如何被编译器修改的?

“数组名被改写成一个指针参数”规则并不是递归定义的。数组的数组会被改写成“数组的指针”,而不是“指针的指针”:

实参                                                      所匹配的形参

数组的数组          char c[8][10];                char (*c)[10];          数组指针  c和*先结合使得arr成为一个指针,这个指针指向具有10个char类型数据的数组


指针数组            char *c[10];                  char **c;              指针的指针

数组指针(行指针)  char (*c)[10];                char (*c)[10];        不改变

指针的指针          char **c;                      char **c;              不改变

下面再看一个网友的一段分析相当给力的代码:

#include "stdafx.h"
#include <iostream>
using namespace std;
 
int _tmain(int argc, _TCHAR* argv[])
{
    int arr1[3];
    int arr2[3];
    int arr3[3];
    int * ptr;
    // ptr1是一个指向 int [3] 的指针,即ptr的类型和&arr1的类型是一样的,注意:arr1指向的内存区域定长
    int ptr1[3][3]={{1,2,3},{1,2,3},{1,2,3}};
    // ptr2是一个指向 int * 的指针,即ptr2的类型和&ptr是一样的,注意:ptr指向的内存区域不定长
    int * ptr2[3]={arr1,arr2,arr3};
    // ptr3是一个指向 int [3] 的指针,即ptr3的类型和&arr1的类型是一样的,注意:arr1指向的内存区域定长
    int(* ptr3)[3]=&arr1;
    ptr3=ptr1; // 没错,他们的类型相同
 // ptr3=ptr2;//error 无法从“int *[3]”转换为“int (*)[3]
 // ptr4是一个指向 int * 的指针,即ptr4的类型和&ptr是一样的,注意:ptr指向的内存区域不定长
    int ** ptr4;
    //ptr4=&arr1; //error 无法从“int (*)[3]”转换为“int **
    ptr4=ptr2; // 没错,他们的类型相同
 //ptr4=ptr3; // error 无法从“int (*)[3]”转换为“int **
    return 0;
}

《C++ 设计新思维》 下载见

C++ Primer Plus 第6版 中文版 清晰有书签PDF+源代码

读C++ Primer 之构造函数陷阱

读C++ Primer 之智能指针

读C++ Primer 之句柄类

将C语言梳理一下,分布在以下10个章节中:

  1. Linux-C成长之路(一):Linux下C编程概要
  2. Linux-C成长之路(二):基本数据类型
  3. Linux-C成长之路(三):基本IO函数操作
  4. Linux-C成长之路(四):运算符
  5. Linux-C成长之路(五):控制流
  6. Linux-C成长之路(六):函数要义
  7. Linux-C成长之路(七):数组与指针
  8. Linux-C成长之路(八):存储类,动态内存
  9. Linux-C成长之路(九):复合数据类型
  10. Linux-C成长之路(十):其他高级议题

本文永久更新链接地址:

相关内容