首先,让我们摆脱那些挑剔的东西。 main 返回 int,不是 void。除非您的编译器文档明确声明它支持void main(),否则请使用int main(void) 或int main(int argc, char **argv)。
现在让我们退后一步,谈谈指针和数组之间的区别。首先要记住的是数组和指针是完全不同的东西。您可能在某处听说或读到数组只是一个指针。这是不正确的。在大多数情况下,数组 表达式 将其类型从“T 的 N 元素数组”隐式转换为“指向 T 的指针”(类型 decays 为指针类型) 并将其值设置为指向数组中的第一件事,例外情况是当数组表达式是 sizeof 或地址 (&) 运算符的操作数时,或者当数组表达式是字符串时用于初始化另一个数组的文字。
数组是一块内存,大小可以容纳 N 个类型为 T 的元素;指针是一块内存,其大小可以保存 T 类型的单个值的地址。您不能将新值分配给数组对象;即,以下是不允许的:
int a[10], b[10];
a = b;
注意字符串字面量(如“aaa”)也是一个数组表达式;类型是 char 的 N 元素数组(C++ 中的 const char),其中 N 是字符串的长度加上终止的 0。字符串文字具有静态范围;它们在程序启动时分配并一直存在到程序退出。它们也是不可写的(尝试修改字符串文字的内容会导致未定义的行为)。例如,表达式“aaa”的类型是具有静态范围的 char 的 4 元素数组。与其他数组表达式一样,字符串文字在大多数情况下从数组类型衰减为指针类型。当你写类似的东西时
char *p = "aaa";
数组表达式“aaa”从char [4]衰减到char *,其值为数组第一个'a'的地址;该地址随后被复制到p。
如果使用字面量来初始化一个 char 数组,那么:
char a[] = "aaa";
然后类型不转换;文字仍然被视为一个数组,并且数组的 contents 被复制到 a (并且 a 隐式调整大小以容纳字符串内容加上 0 终止符)。结果大致相当于写了
char a[4];
strcpy(a, "aaa");
当T a[N] 类型的数组表达式是sizeof 运算符的操作数时,结果是整个数组的大小(以字节为单位):N * sizeof(T)。当它是 address-of (&) 运算符的操作数时,结果是指向整个数组的指针,而不是指向第一个元素的指针(实际上,它们是相同的值,但类型不同):
声明:Ta[N];
表达式类型“衰减”为值
---------- ---- ----------- ------
a T [N] T * a[0] 的地址
&a T (*)[N] a 的地址
sizeof a size_t a 中的字节数
(N * sizeof(T))
a[i] a[i] 的 T 值
&a[i] T * a[i] 的地址
sizeof a[i] size_t a[i] 中的字节数 (sizeof (T))
请注意,数组表达式a 衰减为类型T *,或指向T 的指针。这与表达式&a[0] 的类型相同。这两个表达式都产生数组中第一个元素的地址。表达式&a 的类型为T (*)[N],或指向T 的N 元素数组的指针,它产生数组本身的地址,而不是第一个元素。由于数组的地址与数组的第一个元素的地址相同,a、&a 和&a[0] 都产生相同的值,但表达式不同所有相同的类型。这在尝试将函数定义与函数调用匹配时很重要。如果您想将 array 作为参数传递给函数,例如
int a[10];
...
foo(a);
那么对应的函数定义一定是
void foo(int *p) { ... }
foo 接收的是一个指向 int 的指针,而不是一个 int 数组。请注意,您可以将其称为foo(a) 或foo(&a[0])(甚至foo(&v),其中v 是一个简单的int 变量,尽管如果foo 期望一个会导致问题的数组)。请注意,在函数参数声明的上下文中,int a[] 与 int *a 相同,但在此上下文中仅为真。坦率地说,我认为int a[] 表单是造成对指针、数组和函数的许多困惑思考的原因,应该不鼓励使用它。
如果你想将一个指向数组的指针传递给一个函数,比如
int a[10];
foo(&a);
那么对应的函数定义一定是
void foo(int (*p)[10]) {...}
当你想引用一个特定的元素时,你必须在应用下标之前取消引用指针:
for (i = 0; i < 10; i++)
(*p)[i] = i * i;
现在让我们把活动扳手扔到工作中并为数组添加第二个维度:
声明:Ta[M][N];
表达式类型“衰减”为值
---------- ---- ----------- ------
a[0] 的 T [M][N] T (*)[N] 地址
&a T (*)[M][N] a 的地址
sizeof a size_t a (M * N * sizeof(T)) 中的字节数
a[i] T [N] T * a[i][0] 的地址
&a[i] T (*)[N] a[i] 的地址
sizeof a[i] size_t a[i] 中的字节数 (N * sizeof(T))
a[i][j] a[i][j] 的 T 值
&a[i][j] T * a[i][j] 的地址
请注意,在这种情况下,a 和a[i] 都是数组表达式,因此它们各自的数组类型在大多数情况下都会衰减为指针类型; a 将从“T 的 N 元素数组的 M 元素数组”类型转换为“T 的 N 元素数组的指针”,a[i] 将从“T 的 N 元素数组”类型转换为“指向 T 的指针”。同样,a、&a、a[0]、&a[0] 和 &a[0][0] 都将产生相同的 值(数组开头的地址),但不是所有相同的类型。如果要将二维数组传递给函数,例如:
int a[10][20];
foo(a);
那么对应的函数定义一定是
void foo(int (*p)[20]) {...}
请注意,这与将指针传递给一维数组相同(除了示例中的数组大小不同)。但是,在这种情况下,您可以对指针应用下标,例如
for (i = 0; i < 10; i++)
for (j = 0; j < 20; j++)
p[i][j] = i * j;
在这种情况下,您不必显式取消引用 p,因为表达式 p[i] 隐式地引用它 (p[i] == *(p + i))。
现在让我们看看指针表达式:
声明:T *p;
表达式类型值
---------- ---- ------
p T * 另一个 T 类型对象的地址
*p T 类型的另一个对象的 T 值
&p T ** 指针地址
sizeof p size_t 指针中的字节数(取决于类型和平台,
在常见的桌面架构上介于 4 到 8 之间)
sizeof *p size_t T 中的字节数
sizeof &p size_t 指向指针的字节数(同样,取决于
在类型和平台上)
这一切都很简单。指针类型保存另一个 T 类型对象的地址;取消引用指针 (*p) 会产生该地址的值,并获取指针的地址 (&p) 会产生指针对象的位置(指向指针的指针)。将sizeof 应用于指针值将产生指针中的字节数,而不是指针指向的字节数。
现在,假设您已经做到了这一点并且还没有因为厌倦而死,让我们看看所有这些如何应用于您的代码。
您想要创建一个指向 char 的指针的 数组 并使用三个字符串文字对其进行初始化,因此您可以将其声明为
char *list[] = {"aaa", "bbb", "ccc"};
list 数组的大小已隐式调整为容纳 3 个 char * 类型的元素。尽管字符串字面量“aaa”、“bbb”和“ccc”出现在初始化程序中,但它们并没有用于初始化 char 数组;因此,它们从char [4] 类型的表达式衰减为char * 类型。这些指针值中的每一个都被复制到list 的元素中。
当您将list 传递给函数时,例如
foo(list);
列表的类型从“指向char的4元素数组”(char *[4])衰减为“指向指向char的指针”(char **),所以接收函数必须有一个定义
void foo(char **p) {...}
由于下标是根据指针算术定义的,您可以在指针上使用下标运算符就好像它是char *的数组:
for (i = 0; i < 3; i++)
printf("%s\n", p[i]);
顺便说一句,这就是main 接收argv 的方式,作为指向char 的指针(char **),而不是作为指向char 的指针数组。请记住,就函数参数声明而言,a[] 与*a 相同,因此char *argv[] 与char **argv 相同。
现在,因为我似乎无法停止输入并重新开始工作(追逐死锁不好玩),让我们来探索使用指针和动态分配的内存.
如果您想在运行时动态分配列表(即,您不会提前知道列表中有多少个字符串),您可以将 list 声明为指向 char 的指针,然后调用malloc 为它实际分配内存:
char **list;
size_t number_of_strings;
...
list = malloc(number_of_strings * sizeof *list);
list[0] = "aaa";
list[1] = "bbb";
list[2] = "ccc";
...
由于这些是赋值而不是初始化,文字表达式会衰减为指向 char 的指针,因此我们将“aaa”、“bbb”等的地址复制到list。在这种情况下,list 不是数组类型;它只是一个指向分配在其他地方的一块内存的指针(在这种情况下,来自 malloc 堆)。同样,由于数组下标是根据指针算术定义的,因此您可以将下标运算符应用于指针值就好像它是一个数组。表达式list[i] 的类型是char *。无需担心隐式转换;如果你把它传递给一个函数
foo(list)
那么函数定义就是
void foo(char **list) {...}
你会为列表下标就好像它是一个数组。
pssst……他说完了吗?
是的,我想他已经完成了。