指针
C 语言中的指针基础
原文:https://overiq.com/c-programming-101/pointer-basics-in-c/
最后更新于 2020 年 7 月 27 日
C 的真正力量在于指针。指针一开始有点难掌握。看完指针的基础知识后,你会对它们是什么以及如何使用它们有更好的了解。
什么是指针?
指针是用于存储内存地址的变量。让我们首先了解一下内存在计算机内部是如何组织的。
计算机中的内存是由字节(一个字节由8
位组成)按顺序排列而成的。每个字节都有一个与之相关联的数字,就像数组中的索引或下标一样,这被称为字节的地址。字节的地址从0
开始到比内存小一个字节。例如,假设在 64MB 的内存中,有64 * 2^20 = 67108864
字节。因此这些字节的地址将从0
开始到67108863
。
让我们看看当你声明一个变量时会发生什么。
int marks;
我们知道一个int
占用4
字节的数据(假设我们使用的是 32 位编译器),所以编译器从内存中保留4
连续字节来存储一个整数值。4
分配字节的第一个字节的地址被称为变量marks
的地址。假设4
连续字节的地址为5004
、5005
、5006
和5007
,那么可变标记的地址为5004
。
![](/qrcode.jpg)
地址运算符(&)
为了找到变量的地址,C 提供了一个名为地址运算符(&
)的运算符。要找出可变标记的地址,我们需要在它前面放置&
运算符,如下所示:
&marks
下面的程序演示了如何使用地址运算符(&
)。
// Program to demonstrate address(&) operator
#include<stdio.h>
int main()
{
int i = 12;
printf("Address of i = %u \n", &i);
printf("Value of i = %d ", i);
// signal to operating system program ran fine
return 0;
}
预期输出:
Address of i = 2293340
Value of i = 12
**注意:**每次运行程序时i
的地址可能会有所不同。
工作原理:
要找到变量的地址,在变量名前加&
运算符。关于程序还有一点需要注意的是%u
转换规范的使用。回想一下%u
转换规范用于打印无符号十进制数,由于内存地址不能为负数,所以必须始终使用%u
而不是%d
。
运算符(&
)的地址不能用于常量或表达式,只能用于变量。
&var; // ok
&12; // error because we are using & operator with a constant
&(x+y) // error because we are using & operator with an expression</pre>
我们一直在函数scanf()
中使用地址运算符(&
),不知道为什么?变量的地址被提供给scanf()
,这样它就知道在哪里写数据了。
声明指针变量
如前所述,指针是存储内存地址的变量。就像任何其他变量一样,您需要先声明一个指针变量,然后才能使用它。以下是如何声明指针变量。
语法: data_type *pointer_name;
data_type
是指针的类型(也称为指针的基类型)。
pointer_name
是变量的名称,可以是任何有效的 C 标识符。让我们举一些例子:
int *ip;
float *fp;
int *ip
表示ip
是能够指向int
类型变量的指针变量。换句话说,指针变量ip
只能存储类型为int
的变量的地址。同样,指针变量fp
只能存储类型为float
的变量的地址。变量的类型(也称为基类型)ip
是指向int
的指针,fp
的类型是指向float
的指针。指向 int 类型的指针变量可以象征性地表示为(int *)
。类似地,指针浮动类型的指针变量可以表示为(float *)
。
就像其他变量一样,指针也是一个变量,所以编译器会在内存中保留一些空间。所有指针变量,无论其基类型如何,都将占用相同的内存空间。通常4
字节或2
字节(在 16 位编译器上)用于存储指针变量(这可能因系统而异)。
给指针变量分配地址
声明指针变量后,下一步是给它分配一些有效的内存地址。如果没有给指针变量分配有效的内存地址,就不应该使用指针变量,因为在声明之后,它包含垃圾值,并且可能指向内存中的任何地方。使用未赋值的指针可能会产生不可预测的结果。它甚至可能导致程序崩溃。
int *ip, i = 10;
float *fp, f = 12.2;
ip = &i;
fp = &f;
这里ip
被声明为指向int
的指针,所以它只能指向一个int
变量的内存地址。同样,fp
只能指向一个float
变量的地址。在最后两个语句中,我们已经将i
和f
的地址分别分配给了ip
和fp
。现在,ip
指向变量i
,fp
指向变量f
。需要注意的是,即使您将float
变量的地址分配给int
指针,编译器也不会向您显示任何错误,但您可能不会得到所需的结果。所以一般来说,你应该总是把一个变量的地址分配给相应的相同类型的指针变量。
我们可以在声明时初始化指针变量,但是在这种情况下,变量必须在指针变量之前声明和初始化。
int i = 10, *iptr = &i;
如果一个指针变量的基类型相同,可以将它们的值赋给另一个指针变量。例如:
int marks = 100, *p1, *p2;
p1 = &marks;
p2 = p1;
赋值后,p1
和p2
指向同一个变量marks
。
如前所述,当一个指针变量被声明时,它包含垃圾值,并且它可以指向内存中的任何地方。您可以为任何指针变量分配一个名为NULL
(在stdio.h
中定义)的符号常量。NULL
的赋值保证了指针不指向任何有效的内存位置。
int i = 100, *iptr;
iptr = NULL;
取消指针变量的引用
取消指针变量的引用仅仅意味着访问存储在指针变量中的地址上的数据。到目前为止,我们一直使用变量的名称来访问其中的数据,但是我们也可以使用指针间接访问变量数据。为了实现这一点,我们将使用一个名为间接运算符(*
)的新运算符。通过将间接运算符(*
)放在指针变量之前,我们可以访问地址存储在指针变量中的变量的数据。
int i = 100, *ip = &i;
这里ip
存储变量i
的地址,如果我们把*
放在ip
之前,那么我们就可以访问存储在变量i
中的数据。这意味着下面两个语句做了同样的事情。
printf("%d\n", *ip); // prints 100
printf("%d\n", i); // prints 100
间接运算符(*
)可以作为地址处的值读取。例如,*ip
可以作为地址ip
的值读取。
**注意:**建议千万不要对未初始化的指针变量应用间接操作符,这样做可能会导致意外行为,甚至程序可能会崩溃。
int *ip;
printf("%d", *ip); // WRONG
现在我们知道,通过取消指针变量的引用,我们可以访问存储在指针变量中的地址值。让我们深入了解一下编译器实际上是如何检索数据的。
char ch = 'a';
int i = 10;
double d = 100.21;
char *cp = &ch;
int *ip = &i;
double *ip = &d;
假设指针cp
包含地址1000
。当我们写*cp
时,编译器知道它必须从起始地址1000
检索信息。现在问题来了,从起始地址1000
检索多少数据?1
字节,2
字节;你怎么想呢?为了知道从起始地址1000
检索多少信息,编译器查看指针的基本类型,并将根据指针的基本类型检索信息。例如,如果基本类型是指向char
的指针,则从起始地址检索1
字节的信息,如果基本类型是指向int
的指针,则从起始地址检索4
字节的信息。需要注意的是,如果您所在的系统中int
的大小为2
字节,那么将从起始地址检索2
字节的信息。
因此,在我们的例子中,只有来自起始地址的1
字节的数据将被检索。即只检索存储在地址2000
的数据。
同样,如果ip
指向地址2000
。在编写*ip
时,编译器将从地址 2000 开始检索4
字节的数据。
在下图中,阴影部分显示了检索到的字节数。
在继续之前,请解释以下表达式的含义:
*(&i)
,其中i
是int
类型的变量。
从优先级表中我们知道括号()
的优先级最高,所以&i
先求值。由于&i
是变量i
的地址,因此将其与*
运算符解引用会给出变量i
的值。所以我们可以断定写*(&i)
和写i
是一样的。
下面的例子演示了到目前为止我们所学到的关于指针的一切。
#include<stdio.h>
int main()
{
int i = 12, *ip = &i;
double d = 2.31, *dp = &d;
printf("Value of ip = address of i = %d\n", ip);
printf("Value of fp = address of d = %d\n\n", d);
printf("Address of ip = %d\n", &ip);
printf("Address of dp = %d\n\n", &dp);
printf("Value at address stored in ip = value of i = %d\n", *ip);
printf("Value at address stored in dp = value of d = %f\n\n", *dp);
// memory occupied by pointer variables
// is same regardless of its base type
printf("Size of pointer ip = %d\n", sizeof(ip));
printf("Size of pointer dp = %d\n\n", sizeof(dp));
// signal to operating system program ran fine
return 0;
}
预期输出:
Value of ip = address of i = 2686788
Value of fp = address of d = 1202590843
Address of ip = 2686784
Address of dp = 2686772
Value at address stored in ip = value of i = 12
Value at address stored in dp = value of d = 2.310000
Size of pointer ip = 4
Size of pointer dp = 4
**注意:**每次运行程序时,内存地址可能会有所不同。
以上节目没有什么值得解释的新内容。在我们进入下一章之前,请始终记住指针变量的大小是相同的,不管它的基类型是什么,但是在解引用时将被访问的内存地址的大小取决于指针变量的基类型。
C 语言中的指针算法
原文:https://overiq.com/c-programming-101/pointer-arithmetic-in-c/
最后更新于 2020 年 9 月 24 日
现在你应该知道指针只不过是一个用来存储内存地址的变量。如果这对你来说仍然是新闻,那么在继续本章之前,回去阅读指针基础知识。
在本章中,我们将讨论可以对指针执行的算术运算。
我们不能用指针执行所有类型的算术运算。指针算术与我们日常生活中通常使用的算术略有不同。适用于指针的唯一有效算术运算是:
- 向指针添加整数
- 指针的整数减法
- 减去两个相同类型的指针
指针算术是相对于指针的基本类型执行的。例如,如果我们有一个包含地址1000
的整数指针ip
,那么将其递增1
,我们将得到1004
(即1000 + 1 * 4
)而不是1001
,因为int
数据类型的大小是4
字节。如果我们使用的系统中int
的大小是2
字节,那么我们会得到1002
(即1000 + 1 * 2
)。
同样,递减它,我们将得到996
(即1000 - 1 * 4
)而不是999
。
所以,表达式ip + 4
将指向地址1016
(即1000 + 4 * 4
)。
我们再举几个例子。
int i = 12, *ip = &i;
double d = 2.3, *dp = &d;
char ch = 'a', *cp = &ch;
假设i
、d
、ch
的地址分别为1000
、2000
、3000
,因此ip
、dp
、cp
最初位于1000
、2000
、3000
。
整数上的指针算法
指针表达式 | 如何评价? |
---|---|
ip = ip + 1 |
ip = > ip + 1 = > 1000 + 1*4 = > 1004 |
ip++ 或++ip |
ip++ = > ip + 1 = > 1004 + 1*4 = > 1008 |
ip = ip + 5 |
ip = > ip + 5 = > 1008 + 5*4 = > 1028 |
ip = ip - 2 |
ip = > ip - 2 = > 1028 - 2*4 = > 1020 |
ip-- 或--ip |
ip = > ip + 2 = > 1020 + 2*4 = > 1028 |
浮点指针算法
指针表达式 | 如何评价? |
---|---|
dp + 1 |
dp = dp + 1 =>T1】=>T2】 |
dp++ 或++dp |
dp++ = > dp+1 = > 2008+1*8 = > 2016 |
dp = dp + 5 |
dp = > dp + 5 = > 2016+5*8 = > 2056 |
dp = dp - 2 |
dp = > dp - 2 = > 2056-2*8 = > 2040 |
dp-- 或--dp |
DP = > DP-1=> 2040-1 * 8=> 2032’ |
字符上的指针算法
指针表达式 | 如何评价? |
---|---|
cp + 1 |
cp = cp + 1 =>T1】=>T2】 |
cp++ 或++cp |
cp = > cp + 1 = > 3001 + 1*1 = > 3002 |
cp = cp + 5 |
cp = > cp + 5 = > 3002 + 5*1 = > 3007 |
cp = cp - 2 |
cp = > cp + 5 = > 3007 - 2*1 = > 3005 |
cp-- 或--cp |
cp = > cp + 2 = > 3005 - 1*1 = > 3004 |
**注意:**当我们使用指针算法递增或递减指针变量时,变量i
、d
、ch
的地址不会受到任何影响。
对类型char
的算术运算看似普通的算术运算,因为char
类型的大小是1
字节。另一个需要注意的要点是,当我们通过增加或减少数字来增加或减少指针变量时,指针变量不必仍然指向有效的内存位置。所以,我们在这样移动指针的时候,一定要特别注意。通常,我们对数组使用指针算法,因为数组的元素排列在连续的内存位置,这将在下一章中详细讨论。
下面的程序展示了指针算法。
#include<stdio.h>
int main()
{
int i = 12, *ip = &i;
double d = 2.3, *dp = &d;
char ch = 'a', *cp = &ch;
printf("Value of ip = %u\n", ip);
printf("Value of dp = %u\n", dp);
printf("Value of cp = %u\n\n", cp);
printf("Value of ip + 1 = %u\n", ip + 1);
printf("Value of dp + 1 = %u\n", dp + 1);
printf("Value of cp + 1 = %u\n\n", cp + 1);
printf("Value of ip + 2 = %u\n", ip + 2);
printf("Value of dp + 2 = %u\n", dp + 2);
printf("Value of cp + 2 = %u\n", cp + 2);
return 0;
}
预期输出:
Value of ip = 2293316
Value of dp = 2293304
Value of cp = 2293303
Value of ip + 1 = 2293320
Value of dp + 1 = 2293312
Value of cp + 1 = 2293304
Value of ip + 2 = 2293324
Value of dp + 2 = 2293320
Value of cp + 2 = 2293305
两个指针之间的指针算法
如果我们有两个基类型指针p1
和p2
分别指向地址为1000
和1016
的int
,那么p2 - p1
将给出4
,因为int
类型的大小是4
字节。如果从p1
即p1 - p2
中减去p2
,那么答案将是否定的,即-4
。
下面的程序演示了相同类型的两个指针之间的指针算法。
#include<stdio.h>
int main()
{
int i1 = 12, *ip1 = &i1;
int i2 = 12, *ip2 = &i2;
printf("Value of ip1 or address of i1 = %u\n", ip1);
printf("Value of ip2 or address of i2 = %u\n\n", ip2);
printf("ip2 - ip1 = %d\n", ip1 - ip2);
printf("ip1 - ip2 = %d\n", ip2 - ip1);
// signal to operating system program ran fine
return 0;
}
预期输出:
Value of ip1 or address of i1 = 2686788
Value of ip2 or address of i2 = 2686780
ip2 - ip1 = 2
ip1 - ip2 = -2
组合间接运算符(*)和递增/递减运算符
在处理数组元素时(正如您将在下一章中看到的),C 程序员经常混合使用间接运算符(*
)和递增/递减运算符(++
和--
)。
请始终记住,间接运算符(*
)和递增/递减运算符的优先级是相同的,并且它们是从右向左关联的(参见中的运算符优先级和关联性)。
假设x
是整数变量,p
是指向int
的指针。现在考虑以下陈述,并尝试解释它们。
例 1:
x = *p++;
由于*
和++
操作符具有相同的优先级,从右向左关联++
将应用于p
,而不是*p
。因为递增运算符是后缀,所以表达式中首先使用p
的值,然后它将递增。因此p
指向的第一个整数将被取消引用并分配给x
,然后p
的值将增加1
。
例 2:
x = ++*p;
这里*
运算符首先应用于p
,然后++
应用于*p
。因此,第一个整数指针被取消引用,从取消引用中获得的值递增,并最终分配给x
。
例 3:
x = *++p;
++
运算符有前缀,因此首先,p
将递增,然后新地址的值被取消引用并分配给x
。
**注意:**如果你还有什么疑惑,可以随时用()
围绕你想先评估的表情。
指针比较
可以使用带指针的关系运算符(<
、<=
、>
、>=
、==
、!=
)。==
和!=
运算符用于比较两个指针是否包含相同的地址。当两个指针都为 null 或包含同一变量的地址时,它们是相等的。这些(即==
和!=
)运算符的使用仅在指针属于相同的基类型时有效,或者在空指针和任何其他指针之间有效,或者在空指针(将在后面讨论)和任何其他指针之间有效。只有当两个指针都指向同一个数组的元素时,使用其他关系运算符(<
、<=
、>
、>=
)来比较两个指针才有意义。
指针指向指针
我们知道指针是一个包含内存地址的变量。指针变量本身在内存中占据一些空间,因此它也有一个内存地址。我们可以将指针变量的地址存储在其他变量中,这就是所谓的指针对指针。将指针声明为指针的语法如下:
语法: data_type **p;
让我们举个例子:
int i = 10;
int *ip = &i;
int **iip = &ip;
这里ip
是类型(int *)
或者指向int
的指针,iip
是类型(int **)
或者指向int
的指针。
我们知道*ip
将给出地址ip
的值,即i
的值。你能猜到**iip
会返回什么值吗?
**iip
我们知道间接运算符是从右向左计算的,因此**iip
也可以写成
*(*iip)
*iip
指地址iip
的值或存储在ip
的地址。在取消引用存储在ip
的地址时,我们将获得存储在变量i
中的值。
*(*iip)
=> *ip
=> i
因此**iip
给出存储在变量i
中的值。
下面的程序演示了如何在int
中使用指针对指针。
#include<stdio.h>
int main()
{
int i = 10;
int *ip = &i;
int **iip = &ip;
printf("Value of i = %d\n\n", i);
printf("Address of i = %u\n", &i);
printf("Value of ip = %d\n\n", ip);
printf("Address of ip = %u\n", &ip);
printf("Value of iip = %d\n\n", iip);
printf("Value of *iip = value of ip = %d\n", *iip);
printf("Value of **iip = value of i = %d\n\n", **iip);
return 0;
}
预期输出:
Value of i = 10
Address of i = 2293332
Value of ip = 2293332
Address of ip = 2293320
Value of iip = 2293320
Value of *iip = value of ip = 2293332
Value of **iip = value of i = 10
指针和一维数组
原文:https://overiq.com/c-programming-101/pointers-and-1-d-arrays/
最后更新于 2020 年 7 月 27 日
在 C 语言中,数组的元素存储在连续的存储单元中。例如:如果我们有以下数组。
int my_arr[5] = {
1, 2, 3, 4, 5};
然后,这就是元素在数组中的存储方式。
这里第一个元素在地址5000
,因为每个整数占用4
字节,下一个元素在5004
等等。
在 C 语言中,指针和数组的关系非常密切。我们可以使用指针访问数组的元素。在幕后,编译器还使用指针表示法而不是下标表示法访问数组元素,因为与下标表示法相比,使用指针访问元素非常有效。关于阵列,需要记住的最重要的事情是:
数组的名称是一个常量指针,指向数组第一个元素的地址或数组的基址。
我们可以使用下标符号(即使用方括号)来找到数组元素的地址。例如:
int my_arr[5] = {
11, 22, 33, 44, 55};
这里&my_arr[0]
指向数组第一个元素的地址。由于数组的名称是指向数组第一个元素的常量指针,my_arr
和&my_arr[0]
代表相同的地址。&my_arr[1]
指向第二个元素的地址。类似地&my_arr[2]
指向第三个元素的地址等等。
注: my_arr
为(int *)
类型或指向int
的指针。
下面的程序演示了数组的元素存储在连续的内存位置。
#include<stdio.h>
int main()
{
int my_arr[5] = {
1, 2, 3, 4, 5}, i;
for(i = 0; i < 5; i++)
{
printf("Value of a[%d] = %d\t", i, my_arr[i]);
printf("Address of a[%d] = %u\n", i, &my_arr[i]);
}
// signal to operating system program ran fine
return 0;
}
预期输出:
Value of a[0] = 1 Address of a[0] = 2293312
Value of a[1] = 2 Address of a[1] = 2293316
Value of a[2] = 3 Address of a[2] = 2293320
Value of a[3] = 4 Address of a[3] = 2293324
Value of a[4] = 5 Address of a[4] = 2293328
**注意:**每次运行程序时,内存地址可能会有所不同。
使用指针访问数组中的元素和元素地址
我们知道数组的名字是指向第一个元素的常量指针。考虑以下片段:
int arr[] = {
1,2,3,4,5};
这里arr
是指向第一个元素的指针。但是,指针arr
的基本类型是什么?如果你的答案指向int
或(int *)
。干得好;).
在这种情况下,arr
指向一个整数的地址,即整数1
的地址。所以arr
的基本类型是指向int
或(int*)
的指针。
让我们再举一些例子:
char arr[] = {
'A','B','C','D','E'};
arr
指针的类型是什么?。
这里arr
指向第一个元素的地址,它是一个字符。所以arr
的类型是指向char
或(char *)
的指针。
同样的,
double arr[] = {
1.03, 29.3, 3.42, 49.3, 51.2};
这里arr
是指向double
或(double *)
的指针类型的指针。
**注意:**这些概念是接下来章节的构建模块,所以不要跳过。如果你还不明白,再看一遍。
现在,您可以使用指针算法轻松访问元素的值和地址。假设my_arr
是一组5
整数。
int my_arr[5] = {
11, 22, 33, 44, 55};
这里my_arr
是指向int
或(int *)
的基类型指针的常量指针,根据指针算法,当一个整数加到一个指针上时,我们得到下一个相同基类型元素的地址。所以在上面的例子中,my_arr 指向第一个元素的地址,my_arr+1
指向第二个元素的地址,my_arr + 2
指向第三个元素的地址,以此类推。因此,我们可以得出结论:
my_arr
同&my_arr[0]
my_arr + 1
同&my_arr[1]
my_arr + 2
同&my_arr[2]
my_arr + 3
同&my_arr[3]
my_arr + 4
同&my_arr[4]
总的来说(my_arr + i)
和写&my_arr[i]
是一样的。
现在我们知道如何获取数组中每个元素的地址,通过使用间接运算符(*
)我们可以获取地址处的值。如果我们取消引用my_arr
,那么我们得到数组的第一个元素,即*my_arr
。类似地,*(my_arr + 1)
将返回数组的第二个元素,以此类推。
*(my_arr)
同my_arr[0]
*(my_arr + 1)
同my_arr[1]
*(my_arr + 2)
同my_arr[2]
*(my_arr + 3)
同my_arr[3]
*(my_arr + 4)
同my_arr[4]
总的来说*(my_arr+i)
和写my_arr[i]
是一样的。
以下程序使用指针表示法打印数组元素的值和地址。
#include<stdio.h>
int main()
{
int my_arr[5] = {
1, 2, 3, 4, 5}, i;
for(i = 0; i < 5; i++)
{
printf("Value of a[%d] = %d\t", i, *(my_arr + i) );
printf("Address of a[%d] = %u\n", i, my_arr + i );
}
// signal to operating system program ran fine
return 0;
}
预期输出:
Value of a[0] = 1 Address of a[0] = 2293312
Value of a[1] = 2 Address of a[1] = 2293316
Value of a[2] = 3 Address of a[2] = 2293320
Value of a[3] = 4 Address of a[3] = 2293324
Value of a[4] = 5 Address of a[4] = 2293328
**注意:**每次运行程序时,内存地址可能会有所不同。
将一维数组赋给指针变量
是的,您可以将一维数组赋给指针变量。考虑以下示例:
int *p;
int my_arr[] = {
11, 22, 33, 44, 55};
p = my_arr;
现在,您可以使用指针 p 来访问数组中每个元素的地址和值。需要注意的是,将一维数组分配给指向int
的指针是可能的,因为my_arr
和p
是相同的基类型,即指向int
的指针。一般来说(p+i)
表示 ith 元素的地址,*(p+i)
表示 ith 元素的值。
数组的名字(即my_arr
)和指针变量(即p
)有些区别。数组的名字是一个常量指针,因此你不能改变它来指向其他内存位置。你不能给它分配其他地址,也不能像在指针变量中那样应用递增/递减运算符。
my_arr++; // error
my_arr--; // error
my_arr = &i // error
但是p
是一个普通的指针变量,所以你可以应用指针算法,甚至给它分配一个新的地址。
p++; // ok
p--; // ok
p = &i // ok
下面的程序演示了如何通过将一维数组的元素赋给指针变量来访问作为其地址的值。
#include<stdio.h>
int main()
{
int my_arr[5] = {
1, 2, 3, 4, 5}, i;
int *p;
p = my_arr;
// p = &my_arr[0]; // you can also do this
for(i = 0; i < 5; i++)
{
printf("Value of a[%d] = %d\t", i, *(p + i) );
printf("Address of a[%d] = %u\n", i, p + i );
}
// signal to operating system program ran fine
return 0;
}
预期输出:
Value of a[0] = 1 Address of a[0] = 2293296
Value of a[1] = 2 Address of a[1] = 2293300
Value of a[2] = 3 Address of a[2] = 2293304
Value of a[3] = 4 Address of a[3] = 2293308
Value of a[4] = 5 Address of a[4] = 2293312
**注意:**每次运行程序时,内存地址可能会有所不同。
指针和二维数组
原文:https://overiq.com/c-programming-101/pointers-and-2-d-arrays/
最后更新于 2020 年 7 月 27 日
在最后一章中,我们创建了一个指针,它指向基类型为(int *
)的数组的第 0 个元素或指向int
的指针。我们还可以创建一个指针,它可以指向整个数组,而不是数组中的一个元素。这就是所谓的指向数组的指针。下面是如何声明指向数组的指针。
int (*p)[10];
这里p
是一个可以指向10
整数数组的指针。在这种情况下,p
的类型或基类型是指向10
整数数组的指针。
注意p
周围的圆括号是必须的,所以不能这样做:
int *p[10];
这里p
是10
整数指针的数组。指针数组将在接下来的章节中讨论。
指向数组第 0 个元素的指针和指向整个数组的指针完全不同。下面的程序演示了这个概念。
#include<stdio.h>
int main()
{
int *p; // pointer to int
int (*parr)[5]; // pointer to an array of 5 integers
int my_arr[5]; // an array of 5 integers
p = my_arr;
parr = my_arr;
printf("Address of p = %u\n", p );
printf("Address of parr = %u\n", parr );
p++;
parr++;
printf("\nAfter incrementing p and parr by 1 \n\n");
printf("Address of p = %u\n", p );
printf("Address of parr = %u\n", parr );
printf("Address of parr = %u\n", *parr );
// signal to operating system program ran fine
return 0;
}
预期输出:
Address of p = 2293296
Address of parr = 2293296
After incrementing p and parr by 1
Address of p = 2293300
Address of parr = 2293316
工作原理:
这里p
是指向数组第 0 个元素my_arr
的指针,parr
是指向整个数组my_arr
的指针。p
的基类型是类型(int *
)或指向int
的指针,parr
的基类型是指向5
整数数组的指针。由于指针算术是相对于指针的基本类型执行的,这就是为什么parr
增加20
字节(即5 x 4 = 20
字节)的原因。另一方面,p
只增加4
字节。
关于指向数组的指针,需要记住的重要一点是:
每当一个指向数组的指针被取消引用时,我们就会得到它所指向的数组的地址(或基址)。
因此,在取消引用parr
时,您将获得*parr
。需要注意的重要一点是虽然parr
和*parr
指向同一个地址,但是 parr 的基类型是指向5
整数数组的指针,而*parr
基类型是指向 int 的指针。这是一个重要的概念,将用于访问二维数组的元素。
指针和二维数组
在前面的章节中讨论二维数组时,我们告诉您将二维数组可视化为矩阵。例如:
int arr[3][4] = {
{
11,22,33,44},
{
55,66,77,88},
{
11,66,77,44}
};
上面的二维数组可以可视化如下:
在讨论数组时,我们使用了行和列这样的术语。这个概念只是理论上的,因为计算机内存是线性的,没有行和列。那么二维数组实际上是如何存储在内存中的呢?在 C 语言中,数组是按行顺序存储的。这仅仅意味着第一行 0 被存储,然后紧挨着它的行 1 被存储,紧挨着它的行 2 被存储,以此类推。
下图显示了二维数组是如何存储在内存中的。
关于多维数组,这里是你需要记住的最重要的概念。
二维数组实际上是一维数组,其中每个元素本身就是一维数组。所以 arr 是一个由 3 个元素组成的数组,其中每个元素是一个由 4 个整数组成的一维数组。
在前一章中,我们已经讨论过一维数组的名称是指向第 0 个元素的常量指针。在二维数组的情况下,第 0 个元素是一维数组。因此,在上面的例子中,arr
的类型或基类型是指向4
整数数组的指针。因为指针算术是相对于指针的基本大小来执行的。在arr
的情况下,如果arr
指向地址2000
,那么arr + 1
指向地址2016
(即2000 + 4*4
)。
我们知道数组的名称是指向数组第 0 个元素的常量指针。在二维数组的情况下,第 0 个元素是一维数组。因此在二维数组的情况下,数组的名称表示指向第 0 个一维数组的指针。因此在这种情况下arr
是指向4
元素数组的指针。如果第 0 个 1-D 的地址是2000
,那么根据指针算法(arr + 1
)将代表地址2016
,同样(arr + 2
)将代表地址2032
。
从以上讨论中,我们可以得出结论:
arr
指向第 0 个一维数组。
(arr + 1)
指向 1st 一维阵列。
(arr + 2)
指向第二个一维阵列。
一般来说,我们可以这样写:
(arr + i)
指向一维数组。
正如我们在本章前面讨论的,对数组指针的解引用给出了数组的基址。所以解引用arr
我们会得到*arr
,基础类型*arr
是(int*)
。同样,在取消arr+1
的引用时,我们会得到*(arr+1)
。总的来说,我们可以说:
*(arr+i)
指向第 ith 个一维数组的基址。
同样重要的是要注意类型(arr + i)
和*(arr+i)
指向相同的地址,但是它们的基本类型完全不同。(arr + i)
的基类型是指向 4 个整数的数组的指针,而*(arr + i)
的基类型是指向int
或(int*
)的指针。
那么如何使用 arr 来访问二维数组的各个元素呢?
由于*(arr + i)
指向每个 ith 一维数组的基址,并且是指向int
的基类型指针,通过使用指针算法,我们应该能够访问 ith 一维数组的元素。
让我们看看如何做到这一点:
*(arr + i)
指向一维数组第 0 个元素的地址。因此,
*(arr + i) + 1
指向一维数组第一个元素的地址
*(arr + i) + 2
指向一维数组第二个元素的地址
因此,我们可以得出结论:
*(arr + i) + j
指向一维数组 jth 元素的基址。
在解引用*(arr + i) + j
时,我们将得到一维数组的第一个元素的值。
*( *(arr + i) + j)
利用这个表达式,我们可以求出一维数组的 jth 元素的值。
此外,指针符号*(*(arr + i) + j)
相当于下标符号。
下面的程序演示了如何使用指针表示法访问二维数组元素的值和地址。
#include<stdio.h>
int main()
{
int arr[3][4] = {
{
11,22,33,44},
{
55,66,77,88},
{
11,66,77,44}
};
int i, j;
for(i = 0; i < 3; i++)
{
printf("Address of %d th array %u \n",i , *(arr + i));
for(j = 0; j < 4; j++)
{
printf("arr[%d][%d]=%d\n", i, j, *( *(arr + i) + j) );
}
printf("\n\n");
}
// signal to operating system program ran fine
return 0;
}
预期输出:
Address of 0 th array 2686736
arr[0][0]=11
arr[0][1]=22
arr[0][2]=33
arr[0][3]=44
Address of 1 th array 2686752
arr[1][0]=55
arr[1][1]=66
arr[1][2]=77
arr[1][3]=88
Address of 2 th array 2686768
arr[2][0]=11
arr[2][1]=66
arr[2][2]=77
arr[2][3]=44
将二维数组赋给指针变量
您可以将数组的名称分配给指针变量,但与一维数组不同,您需要指向数组的指针,而不是指向int
或(int *
)的指针。这里有一个例子:
int arr[2][3] = {
{
33, 44, 55},
{
11, 99, 66}
};
请始终记住,二维数组实际上是一维数组,其中每个元素都是一维数组。所以arr
是一个由2
个元素组成的数组,其中每个元素是一个由 3 个整数组成的一维arr
。因此,为了存储arr
的基址,您需要一个指向3
整数数组的指针。
同样,如果二维数组有3
行和4
列,即int arr[3][4]
,那么你需要一个指向4
整数数组的指针。
int (*p)[3];
这里p
是一个指向3
整数数组的指针。所以根据指针算法p+i
指向第 I 个一维数组,换句话说,p+0
指向第 0 个一维数组,p+1
指向第 1 个一维数组,以此类推。(p+i
)的基本类型是指向3
整数数组的指针。如果我们取消引用(p+i
),那么我们将得到一维数组的基地址,但是现在*(p + i)
的基类型是指向int
或(int *
)的指针。再次访问具有一维数组的 jth 元素的地址,我们只需将j
添加到*(p + i)
。所以*(p + i) + j
指向一维数组的 jth 元素的地址。因此表达式*(*(p + i) + j)
给出了一维数组的 jth 元素的值。
下面的程序演示了如何使用指向数组的指针来访问二维数组的元素。
#include<stdio.h>
int main()
{
int arr[3][4] = {
{
11,22,33,44},
{
55,66,77,88},
{
11,66,77,44}
};
int i, j;
int (*p)[4];
p = arr;
for(i = 0; i < 3; i++)
{
printf("Address of %d th array %u \n",i , p + i);
for(j = 0; j < 4; j++)
{
printf("arr[%d][%d]=%d\n", i, j, *( *(p + i) + j) );
}
printf("\n\n");
}
// signal to operating system program ran fine
return 0;
}
预期输出:
Address of 0 th array 2686736
arr[0][0]=11
arr[0][1]=22
arr[0][2]=33
arr[0][3]=44
Address of 1 th array 2686752
arr[1][0]=55
arr[1][1]=66
arr[1][2]=77
arr[1][3]=88
Address of 2 th array 2686768
arr[2][0]=11
arr[2][1]=66
arr[2][2]=77
arr[2][3]=44
C 语言中的按值调用和按引用调用
原文:https://overiq.com/c-programming-101/call-by-value-and-call-by-reference-in-c/
最后更新于 2020 年 7 月 27 日
c 提供了两种向函数传递参数的方式。
- 按值调用或按值传递。
- 通过引用调用。
让我们从“价值召唤”开始。
按值调用
在这种方法中,首先复制每个实际参数,然后将这些值分配给相应的形式参数。
这意味着被调用函数所做的更改不会影响调用函数中实际参数的值。在上图所示的示例中,my_func()
函数修改了val1
和val2
值的副本。但是val1
和val2
的原值不变。
到目前为止,我们编写的所有函数都使用按值调用,除了我们向函数传递数组的函数。
引用调用
在这种方法中,实际参数的地址被复制,然后分配给相应的形式参数。现在,形式参数和实际参数都指向相同的数据(因为它们包含相同的地址)。因此,被调用函数所做的任何更改也会影响实际参数。
让我们举一些例子:
以下程序演示了按值调用:
#include<stdio.h>
void try_to_change(int, int);
int main()
{
int x = 10, y = 20;
printf("Initial value of x = %d\n", x);
printf("Initial value of y = %d\n", y);
printf("\nCalling the function\n");
try_to_change(x, y);
printf("\nValues after function call\n\n");
printf("Final value of x = %d\n", x);
printf("Final value of y = %d\n", y);
// signal to operating system program ran fine
return 0;
}
void try_to_change(int x, int y)
{
x = x + 10;
y = y + 10;
printf("\nValue of x (inside function) = %d\n", x);
printf("Value of y (inside function) = %d\n", y);
}
预期输出:
Initial value of x = 10
Initial value of y = 20
Value of x (inside function) = 20
Value of y (inside function) = 30
Values after function call
Final value of x = 10
Final value of y = 20
工作原理:
函数main()
内部的变量x
和y
与函数try_to_change()
形式自变量中的变量x
和y
完全不同。第 13 行,调用try_to_change()
函数时,复制x
和y
的值,并将该副本传递给函数try_to_change()
的形式参数x
和y
。在函数try_to_change()
中,我们试图通过为其分配新值来更改x
和y
的原始值。由于try_to_change()
正在处理x
和y
的副本,因此try_to_change()
函数所做的更改不会对实际参数x
和y
产生影响。
要使用引用调用,我们需要做两件事:
- 传递实际参数的地址,而不是将值传递给函数。
- 将函数的形式参数声明为适当类型的指针变量。
下面的程序演示了引用调用。
#include<stdio.h>
void try_to_change(int *, int *);
int main()
{
int x = 10, y = 20;
printf("Initial value of x = %d\n", x);
printf("Initial value of y = %d\n", y);
printf("\nCalling the function\n");
try_to_change(&x, &y);
printf("\nValues after function call\n\n");
printf("Final value of x = %d\n", x);
printf("Final value of y = %d\n", y);
// signal to operating system everything works fine
return 0;
}
void try_to_change(int *x, int *y)
{
(*x)++;
(*y)++;
printf("\nValue of x (inside function) = %d\n", *x);
printf("Value of y (inside function) = %d\n", *y);
}
预期输出:
Initial value of x = 10
Initial value of y = 20
Calling the function
Value of x (inside function) = 11
Value of y (inside function) = 21
Values after function call
Final value of x = 11
Final value of y = 21
这里我们将整数变量的地址传递给一个函数。所以形式参数必须声明为指向int
或(int *)
的指针。表达式(*x)++
意味着首先在x
取消引用该值,然后增加它。类似地,(*y)++
意味着首先在y
取消引用该值,然后增加它。当功能try_to_change()
结束时,控制返回到第 17 行和第 18 行的main()
和printf()
语句,分别打印x
和y
的新值。
从 C 语言中的函数返回多个值
原文:https://overiq.com/c-programming-101/returning-more-than-one-value-from-function-in-c/
最后更新于 2020 年 7 月 27 日
在 C 语言的 return 语句一章中,我们了解到 Return 语句用于从函数中返回值。但是有一个限制,一个return
语句只能从一个函数中返回一个值。在本章中,我们将看到如何通过引用调用来克服这个限制。
考虑以下问题。
Q -创建一个函数,返回传递给它的两个数的和、差、积。
告诉我你将如何处理这个问题?
解决这个问题的一种方法是为 3 个操作创建三个函数,然后在每个函数中使用 return 语句返回 sum、difference 和乘积。通过引用调用,我们可以很容易地解决这个问题。
下面的程序演示了如何通过引用调用从一个函数返回多个值。
#include<stdio.h>
void return_more_than_one(int a, int b, int *sum, int *diff, int *prod);
int main()
{
int x = 40, y = 10, sum, diff, prod;
return_more_than_one(x, y, &sum, &diff, &prod);
printf("%d + %d = %d\n",x, y, sum);
printf("%d - %d = %d\n",x, y, diff);
printf("%d * %d = %d\n",x, y, prod);
// signal to operating system program ran fine
return 0;
}
void return_more_than_one(int a, int b, int *sum, int *diff, int *prod)
{
*sum = a+b;
*diff = a-b;
*prod = a*b;
}
预期输出:
40 + 10 = 50
40 - 10 = 30
40 * 10 = 400
工作原理:
在return_more_than_one()
中,函数 a 和 b 通过值调用传递,而sum
、diff
和prod
通过引用调用传递。因此return_more_than_one()
函数知道sum
、diff
和prod
变量的地址,所以它使用指针间接访问这些变量并改变它们的值。
从 C 语言中的函数返回指针
原文:https://overiq.com/c-programming-101/returning-a-pointer-from-a-function-in-c/
最后更新于 2020 年 7 月 27 日
我们已经看到一个函数可以返回 int、float、char 等类型的数据。同样,函数可以返回指向数据的指针。函数返回指针的语法如下。
语法: type *function_name(type1, type2, ...);
一些例子:
int *func(int, int); // this function returns a pointer to int
double *func(int, int); // this function returns a pointer to double
下面的程序演示了如何从函数返回指针。
#include<stdio.h>
int *return_pointer(int *, int); // this function returns a pointer of type int
int main()
{
int i, *ptr;
int arr[] = {
11, 22, 33, 44, 55};
i = 4;
printf("Address of arr = %u\n", arr);
ptr = return_pointer(arr, i);
printf("\nAfter incrementing arr by 4 \n\n");
printf("Address of ptr = %u\n\n" , ptr);
printf("Value at %u is %d\n", ptr, *ptr);
// signal to operating system program ran fine
return 0;
}
int *return_pointer(int *p, int n)
{
p = p + n;
return p;
}
预期输出:
Address of arr = 2686736
After incrementing arr by 4
Address of ptr = 2686752
Value at 2686752 is 55
工作原理:
因为数组的名称是指向数组第 0 个元素的指针。这里我们将两个参数传递给函数return_pointer()
。通过引用调用传递arr
(注意数组的名称前面没有&
运算符,因为数组的名称是指向一维数组第 0 个元素的常量指针),通过值调用传递i
。在功能指针内p
增加n
并重新分配到p
。最后,指针p
返回到main()
功能并重新分配给ptr
。
永远不要从函数返回指向局部变量的指针。
考虑下面的代码。
#include<stdio.h>
int *abc(); // this function returns a pointer of type int
int main()
{
int *ptr;
ptr = abc();
return 0;
}
int *abc()
{
int x = 100, *p;
p = &x;
return p;
}
你能指出上面代码的问题吗?
在函数abc()
中,我们返回一个指向局部变量的指针。回想一下,局部变量只存在于函数内部,一旦函数结束,变量x
就不复存在,因此指向它的指针只在函数abc()
内部有效。
即使abc()
返回的地址分配给了main()
内部的ptr
,但是ptr
指向的变量不再可用。取消引用ptr
你会得到一些垃圾值。
**注:**有时你甚至可能得到正确的答案即100
,但你绝不能依赖这种行为。
将一维数组传递给 C 语言中的函数
原文:https://overiq.com/c-programming-101/passing-1-d-array-to-a-function-in-c/
最后更新于 2020 年 7 月 27 日
在 C 语言的一维数组和函数一章中,我们讨论了当一个数组传递给一个函数时,该函数所做的改变会影响原始数组。在研究了指针之后,我们能够理解为什么会发生这种情况。但是在我们研究这个之前,我想明确几点。
在上述章节中,我们还了解到,当一维数组传递给函数时,可以选择在形式参数中指定数组的大小。因此,如果我们传递一个 5 个整数的数组,那么函数的形式参数可以用以下两种方式编写。
int my_arr[5] = [11,44,66,90,101];
第一种方式:
void function(int a[]) // here the size of the array is omitted
{
// statements;
}
第二种方式:
void function(int a[5]) // here the size of the array is specified
{
// statements;
}
在指针和一维数组一章中,我们还了解到数组的名称是指向数组第 0 个元素的常量指针。在我们的例子中my_arr
是指向数组第 0 个元素的指针,换句话说,my_arr
指向元素 11 的地址。所以my_arr
的基本类型是指向int
或(int *)
的指针。因此,函数的形式参数也可以声明为指向int
或(int *)
的指针:
第三种方式:
void function(int *a)
{
// statements;
}
本质上,在所有这三种情况下,a
的基本类型是指向int
或(int *)
的指针,我们只是用三种不同的方式来表示它们。
好了,让我们回到最初的讨论:为什么对函数内部的数组所做的更改会影响原始数组?以下程序回答了这个问题。
#include<stdio.h>
void new_array(int a[]);
int main()
{
int my_arr[] = {
1,4,9,16,23}, i;
printf("Original array: \n\n");
for(i = 0; i < 5; i++)
{
printf("%d ", my_arr[i]);
}
my_func(my_arr);
printf("\n\nModified array : \n\n");
for(i = 0; i < 5; i++)
{
printf("%d ", my_arr[i]);
}
// signal to operating system program ran fine
return 0;
}
void my_func(int a[5])
{
int i;
// increment original elements by 5
for(i = 0; i < 5; i++)
{
a[i] = a[i] + 5;
}
}
预期输出:
Original array:
1 4 9 16 23
Modified array:
6 9 14 21 28
工作原理:
我们知道my_arr
是指向数组第一个元素的指针。所以我们可以不使用&
运算符将my_arr
传递给功能my_func()
。在第 15 行中,my_func()
用一个实际的参数my_arr
来调用,然后分配给a
。再次注意,我们将my_arr
的地址传递给a
,这意味着我们使用的是引用调用,而不是值调用。所以现在my_arr
和a
都指向同一个数组。在函数内部,我们使用 for 循环将数组的每个元素增加5
。因为我们是在原始数组上操作,所以这里所做的所有更改都会影响原始数组。
将二维数组传递给 C 语言中的函数
原文:https://overiq.com/c-programming-101/passing-2-d-array-to-a-function-in-c/
最后更新于 2020 年 7 月 27 日
就像一维数组一样,当二维数组传递给函数时,函数所做的更改会影响原始数组。但是在我们研究这个之前,我想明确几点。
我们在第章中了解到,当二维数组传递给函数时,可以选择指定最左边维度的大小。因此,如果我们有一个 2 行 3 维的数组,那么它可以通过以下两种方式传递给函数:
int two_d[2][3] = {
{
99,44,11},
{
4,66,9}
};
第一种方式:
void function(int a[][3])
{
// statements;
}
第二种方式:
void function(int a[2][3])
{
// statements;
}
回想一下,二维数组是按行主顺序存储的,即第一行 0 被存储,然后紧挨着它的第一行 1 被存储,以此类推。因此,在 C 语言中,二维数组实际上是一维数组,其中每个元素本身都是一维数组。因为数组的名称指向数组的第 0 个元素。在二维数组的情况下,第 0 个元素是数组。因此,从这个讨论中,我们可以得出two_d
是指向 3 个整数的数组的指针。
因此,我们也可以声明一个函数,其中形式参数是指向数组的指针类型。
第三种方式:
void function(int (*a)[3])
{
// statements;
}
本质上,在讨论的所有三种情况下,变量a
的类型是指向 3 个整数的数组的指针,它们的区别仅在于它们的表示方式。
好了,让我们回到最初的讨论——为什么函数所做的更改会影响原始数组?以下程序回答了这个问题。
#include<stdio.h>
void change_twod(int (*a)[3]);
int main()
{
int i,j, two_d[2][3] = {
{
99,44,11},
{
4,66,9}
};
printf("Original array: \n\n");
for(i = 0; i < 2; i++)
{
for(j = 0; j < 3; j++)
{
printf("%3d ", two_d[i][j]);
}
printf("\n");
}
change_twod(two_d);
printf("\n\nModified array : \n\n");
for(i = 0; i < 2; i++)
{
for(j = 0; j < 3; j++)
{
printf("%3d ", two_d[i][j]);
}
printf("\n");
}
// signal to operating system everything works fine
return 0;
}
void change_twod(int (*arr)[3])
{
int i, j;
printf("\n\nIncrementing every element by 5\n");
// increment original elements by 6
for(i = 0; i < 2; i++)
{
for(j = 0; j < 3; j++)
{
arr[i][j] = arr[i][j] + 5;
}
}
}
预期输出:
Original array:
99 44 11
4 66 9
Incrementing every element by 5
Modified array :
104 49 16
9 71 14
工作原理:
如本节前面所讨论的,two_d
和arr
是指向一个3
整数数组的指针类型。在第 25 行中,change_twod()
用two_d
的实际参数调用,然后分配给arr
。现在two_d
和arr
都指向同一个二维数组,因此,函数内部所做的更改将在函数main()
中可见。
C 语言中的指针数组
原文:https://overiq.com/c-programming-101/array-of-pointers-in-c/
最后更新于 2020 年 7 月 27 日
就像我们可以声明一个由int
、float
或char
等组成的数组一样,我们也可以声明一个指针数组,下面是做同样事情的语法。
语法: datatype *array_name[size];
让我们举个例子:
int *arrop[5];
这里arrop
是5
整数指针的数组。这意味着这个数组可以保存5
整数变量的地址。换句话说,您可以将指向int
类型指针的5
指针变量分配给该数组的元素。
下面的程序演示了如何使用指针数组。
#include<stdio.h>
#define SIZE 10
int main()
{
int *arrop[3];
int a = 10, b = 20, c = 50, i;
arrop[0] = &a;
arrop[1] = &b;
arrop[2] = &c;
for(i = 0; i < 3; i++)
{
printf("Address = %d\t Value = %d\n", arrop[i], *arrop[i]);
}
return 0;
}
预期输出:
Address = 387130656 Value = 10
Address = 387130660 Value = 20
Address = 387130664 Value = 50
工作原理:
注意我们如何分配a
、b
和c
的地址。在第 9 行,我们将变量a
的地址分配给数组的第 0 个元素。类似地,b
和c
的地址分别分配给第一和第二元素。此时,arrop
看起来是这样的:
arrop[i]
给出数组第 I 个元素的地址。所以arrop[0]
返回变量a
的地址,arrop[1]
返回b
的地址等等。要获取地址值,请使用间接运算符(*
)。
*arrop[i]
因此,*arrop[0]
给出地址arrop[0]
处的值,类似地*arrop[1]
给出地址arrop[1]
处的值,以此类推。
C 语言中的空指针
最后更新于 2020 年 7 月 27 日
我们在 C 语言的指针基础一章中了解到,如果一个指针是指向int
或(int *)
的指针类型,那么它只能保存int
类型变量的地址。如果我们将一个float
变量的地址分配给一个指向int
的指针类型的指针,这将是不正确的。但是void
指针是这个规则的例外。一个void
指针可以指向任何数据类型的变量。以下是void
指针的语法。
语法: void *vp;
让我们举个例子:
void *vp;
int a = 100, *ip;
float f = 12.2, *fp;
char ch = 'a';</pre>
这里vp
是一个void
指针,所以你可以给它分配任何类型变量的地址。
vp = &a; // ok
vp = ip; // ok
vp = fp; // ok
ip = &f; // wrong since type of ip is pointer to int
fp = ip; // wrong since type of fp is pointer to float</pre>
void
指针可以指向任何数据类型的变量,void
指针可以被分配给任何类型的指针。
取消引用空指针
我们不能仅仅使用间接(*
)操作符来取消引用一个空指针。例如:
void *vp;
int a = 100;
vp = &a;
printf("%d", *vp); // wrong
它根本不是那样工作的!。在取消引用空指针之前,必须将其类型转换为适当的指针类型。让我告诉你我的意思。
例如:在上面的片段中void
指针vp
指向整数变量 a 的地址,所以在这种情况下vp
充当指向int
或(int *)
的指针。因此在这种情况下正确的类型是(int*)
。
(int *)vptr
现在vptr
的类型暂时从void
指针变为指向int
或(int*)
的指针,我们已经知道如何去引用指向int
的指针,只需在它前面加上间接操作符(*
)
*(int *)vptr
**注意:**类型转换暂时改变vp
的类型,直到表达式求值,程序中的其他地方vp
仍然是一个空指针。
下面的程序演示了如何取消引用void
指针。
#include<stdio.h>
#define SIZE 10
int main()
{
int i = 10;
float f = 2.34;
char ch = 'k';
void *vptr;
vptr = &i;
printf("Value of i = %d\n", *(int *)vptr);
vptr = &f;
printf("Value of f = %.2f\n", *(float *)vptr);
vptr = &ch;
printf("Value of ch = %c\n", *(char *)vptr);
// signal to operating system program ran fine
return 0;
}
预期输出:
Value of i = 10
Value of f = 2.34
Value of ch = k
空指针中的指针算法
我想提的另一个重要的点是关于带 void 指针的指针算法。在 void 指针中应用指针算法之前,请确保首先提供正确的类型转换,否则可能会得到未接受的结果。
考虑以下示例:
int one_d[5] = {
12, 19, 25, 34, 46}, i;
void *vp = one_d;
printf("%d", one_d + 1); // wrong
这里我们已经将数组的名称one_d
分配给了空指针vp
。由于one_d
的基本类型是指向int
或(int*)
的指针,因此空指针vp
的作用类似于指向int
或(int*)
的指针。所以合适的类型是(int*)
。
int one_d[5] = {
12, 19, 25, 34, 46}, i;
void *vp = one_d;
printf("%d", (int *)one_d + 1); // correct
下面的程序演示了 void 指针中的指针算法。
#include<stdio.h>
#define SIZE 10
int main()
{
int one_d[5] = {
12, 19, 25, 34, 46}, i;
void *vp = one_d;
for(i = 0; i < 5; i++)
{
printf("one_d[%d] = %d\n", i, *( (int *)vp + i ) );
}
// signal to operating system program ran fine
return 0;
}
预期输出:
one_d[0] = 12
one_d[1] = 19
one_d[2] = 25
one_d[3] = 34
one_d[4] = 46
void 指针在动态内存分配中被广泛使用,我们将在下面讨论。
C 语言的malloc()
函数
原文:https://overiq.com/c-programming-101/the-malloc-function-in-c/
最后更新于 2020 年 7 月 27 日
到目前为止,在我们的程序中,我们一直使用静态内存分配。在静态内存分配中,程序的大小是固定的,我们不能在程序运行时增加或减少大小。那么,为什么我们要在程序运行时增加或减少程序的大小呢?
考虑以下情况。
假设我们正在创建一个程序来计算一个班级学生的平均分数。这里有一个解决问题的方法。
#include<stdio.h>
#define STUDENT 100
int main()
{
float marks[STUDENT], sum = 0;
int i;
for(i = 0; i < STUDENT; i++)
{
printf("Enter marks for %d student: ", i+1);
scanf("%f", &marks[i]);
}
// calculate sum
for(i = 0; i < STUDENT; i++)
{
sum += marks[i];
}
printf("\nAverage marks = %.2f\n", sum/STUDENT );
// signal to operating system everything works fine
return 0;
}
关于程序要注意的重要一点是学生的大小是固定的也就是100
。
此时,可能会出现两种类型的问题。假设又有 20 名学生加入了这个班。由于我们的程序只能处理 100 名学生,解决这个问题的一种方法是改变学生的大小,重新编译并再次运行程序。如果过了一段时间,又有 50 名学生加入了这个班,那么我们必须修改程序并重新编译。当然,这不是理想的方式。
让我们面对硬币的另一面。如果有 40 个学生离开了这个班。在这种情况下,要存储的值的数量小于数组的大小,因此(40*4 = 160 字节)内存将被浪费。
正如你所看到的,我们的程序由于阵列大小固定面临两大缺点。
那么解决办法是什么呢?
解决方案是使用动态内存分配。它只是意味着我们可以在程序运行时随时分配/释放内存。
内存的分配/释放是借助头文件 stdlib.h 中定义的三个函数来完成的。
每当您调用这些函数时,它们都会从称为堆的内存区域中获取内存,并在不需要时释放内存,这样就可以重用它。
malloc()函数
它用于在运行时分配内存。该函数的语法是:
语法: void *malloc(size_t size);
该函数接受一个名为size
的参数,该参数的类型为size_t
。size_t
在stdlib.h
中被定义为unsigned int
,现在,你可以把它当成unsigned int
的别名。
如果成功,malloc()
返回一个指向内存第一个分配字节的空指针。在使用指针之前,必须将其转换为适当的类型。所以malloc()
功能一般使用如下:
p = (datatype *)malloc(size);
其中p
是类型为(datatype *)
的指针,size
是您想要分配的内存空间(以字节为单位)。
让我们举一个简单的例子:
假设我们想使用malloc()
动态分配20
字节(用于存储5
整数,其中每个整数的大小为4
字节)。我们可以这样做:
int *p; // p is pointer to int or (int*)
p = (int*)malloc(20); // allocate 20 bytes
该语句从堆中分配20
个连续字节的内存,并将第一个字节的地址分配给变量p
。注意从malloc()
函数返回的 void 指针是如何被类型化然后分配给p
的。分配的内存包含垃圾值,因此在为其分配适当的值之前,不要尝试取消引用它。
正如我们所知,C 语言中数据类型的大小因系统而异,这就是为什么malloc()
函数与sizeof
运算符结合使用的原因。
int *p; // p is pointer to int or (int*)
p = (int*)malloc(5*sizeof(int)); // allocate sufficient memory for 5 integers
我们仍在分配20
字节的内存,但现在我们的程序是可移植的(即它可以在各种操作系统上运行,无需任何修改。)当然更易读。
现在我们有p
指向分配内存的第一个字节,我们可以使用指针算法轻松访问后续字节。
当堆用完空闲空间时,malloc()
函数返回NULL
。所以在以任何方式使用指针变量之前,我们必须首先检查malloc()
函数返回的值。
if(p == NULL)
{
printf("Memory allocation failed");
exit(1); // exit the program
}
让我们重写程序,使用malloc()
函数计算一个班级学生的平均成绩。
#include<stdio.h>
#include<stdlib.h>
int main()
{
float *p, sum = 0;
int i, n;
printf("Enter the number of students: ");
scanf("%d", &n);
// allocate memory to store n variables of type float
p = (float*)malloc(n*sizeof(float));
// if dynamic allocation failed exit the program
if(p==NULL)
{
printf("Memory allocation failed");
exit(1); // exit the program
}
// ask the student to enter marks
for(i = 0; i < n; i++)
{
printf("Enter marks for %d student: ", i+1);
scanf("%f", p+i);
}
// calculate sum
for(i = 0; i < n; i++)
{
sum += *(p+i);
}
printf("\nAverage marks = %.2f\n", sum/n);
// signal to operating system program ran fine
return 0;
}
预期输出:
Enter the number of students: 4
Enter marks for 1 student: 12.12
Enter marks for 2 student: 34.14
Enter marks for 3 student: 43.1
Enter marks for 4 student: 45.87
Average marks = 33.81
2nd run:
Enter the number of students: 2
Enter marks for 1 student: 13.41
Enter marks for 2 student: 56.31
Average marks = 34.86
工作原理:
在第 6 行,我们声明了一个指向float p
和a
浮动变量s
的指针,在这里它被初始化为0
。
在第 7 行,我们已经声明了两个类型为int
的变量i
和n
。
在第 9 行中,printf()
功能将"Enter the number of students: "
打印到控制台。
在第 10 行中,scanf()
用于读取来自用户的输入,然后存储在变量n
中。
第 12 行使用malloc()
功能动态分配内存来存储float
类型的n
号。变量p
是指向float
或(float*)
的类型指针,这就是为什么malloc()
函数的结果是使用(float*)
类型铸造的。
第 15 行,if 条件检查malloc()
返回的指针是否为空指针。如果p
是NULL
,则内存分配失败,程序终止。
在第 21 行,我们有一个 for 循环,它反复要求用户输入 n 次标记。注意在scanf()
语句中p + i
不用&
符号,因为p
是指针。
在第 29 行,我们有另一个 for 循环,它将n
学生的marks
累加到变量sum
中。
在第 34 行中,平均分数是用总分数除以学生总数显示的。
C 中的calloc()
函数
原文:https://overiq.com/c-programming-101/the-calloc-function-in-c/
最后更新于 2020 年 7 月 27 日
c 提供了另一个动态分配内存的函数,它有时比 malloc()函数更好。它的语法是:
语法: void *calloc(size_t n, size_t size);
它接受两个参数第一个参数是元素的数量,第二个参数是元素的大小。假设我们想为5
整数分配内存,在这种情况下,5
是元素的数量,即n
,每个元素的大小是4
字节(可能因系统而异)。以下是如何使用calloc()
为 5 个整数分配内存。
int *p;
p = (int*)calloc(5, 4);
这将从堆中分配20
字节的连续内存空间,并将第一个分配字节的地址分配给指针变量p
。
以下是如何使用malloc()
功能实现同样的事情:
int *p;
p = (int*)malloc(5 * 4);
为了使我们的程序可移植性和可读性更强sizeof()
运算符与calloc()
一起使用。
int *p;
p = (int*)calloc(5, sizeof(int));
那么除了争论的数量之外calloc()
和malloc()
还有其他区别吗?
calloc()
和malloc()
函数的区别在于malloc()
分配的内存包含垃圾值,而calloc()
分配的内存总是初始化为0
。
以下程序使用calloc()
创建动态(运行时大小可以变化)一维数组。
#include<stdio.h>
#include<stdlib.h>
int main()
{
int *p, i, n;
printf("Enter the size of the array: ");
scanf("%d", &n);
p = (int*)calloc(n, sizeof(int));
if(p==NULL)
{
printf("Memory allocation failed");
exit(1); // exit the program
}
for(i = 0; i < n; i++)
{
printf("Enter %d element: ", i);
scanf("%d", p+i);
}
printf("\nprinting array of %d integers\n\n", n);
// calculate sum
for(i = 0; i < n; i++)
{
printf("%d ", *(p+i));
}
// signal to operating system program ran fine
return 0;
}
**预期输出:**第一次运行:
Enter the size of the array: 5
Enter 0 element: 13
Enter 1 element: 24
Enter 2 element: 45
Enter 3 element: 67
Enter 4 element: 89
printing array of 5 integers
13 24 45 67 89
第二次运行:
Enter the size of the array: 2
Enter 0 element: 11
Enter 1 element: 34
printing array of 2 integers
11 34
C 语言中的realloc()
函数
原文:https://overiq.com/c-programming-101/the-realloc-function-in-c/
最后更新于 2020 年 7 月 27 日
假设我们已经使用malloc()
和calloc()
分配了一些内存,但是后来我们发现内存太大或太小。realloc()函数用于在不丢失旧数据的情况下调整已分配内存的大小。它的语法是:
语法: void *realloc(void *ptr, size_t newsize);
realloc()
函数接受两个参数,第一个参数ptr
是一个指针,指向先前使用malloc()
或calloc()
函数分配的内存的第一个字节。newsize 参数以字节为单位指定块的新大小,该大小可以小于或大于原始大小。而size_t
只是stdlib.h
头文件里面定义的unsigned int
的别名。
让我们举个例子:
int *p;
p = (int*)malloc(5*sizeof(int)); // allocate memory for 5 integers</pre>
假设以后有时候我们想增加分配内存的大小来存储6
更多的整数。为此,我们必须分配额外的6 x sizeof(int)
字节内存。下面是如何调用realloc()
函数来分配6 x sizeof(int)
字节的内存。
// allocate memory for 6 more integers integers i.e a total of 11 integers
p = (int*)realloc(p, 11*sizeof(int));
如果在已经使用的字节之后有足够的内存(在这种情况下是6 * sizeof(int)
字节)可用,那么realloc()
函数只分配已经使用的字节旁边的6 * sizeof(int)
字节。在这种情况下,ptr
指向的记忆不会改变。需要注意的是,这样做不会丢失旧数据,但新分配的字节不会初始化。
另一方面,如果在已经使用的字节之后没有足够的内存(在这种情况下为6 * sizeof(int)
字节),则realloc()
在堆的其他地方重新分配整个11 * sizeof(int)
字节的内存,并将内容从旧的内存块复制到新的内存块。在这种情况下,ptr
所指向的地址发生变化。
如果realloc()
未能按照请求扩展内存,则返回NULL
,旧内存中的数据不受影响。
以下程序演示了realloc()
功能。
#include<stdio.h>
#include<stdlib.h>
int main()
{
int *p, i, n;
printf("Initial size of the array is 4\n\n");
p = (int*)calloc(4, sizeof(int));
if(p==NULL)
{
printf("Memory allocation failed");
exit(1); // exit the program
}
for(i = 0; i < 4; i++)
{
printf("Enter element at index %d: ", i);
scanf("%d", p+i);
}
printf("\nIncreasing the size of the array by 5 elements ...\n ");
p = (int*)realloc(p, 9 * sizeof(int));
if(p==NULL)
{
printf("Memory allocation failed");
exit(1); // exit the program
}
printf("\nEnter 5 more integers\n\n");
for(i = 4; i < 9; i++)
{
printf("Enter element at index %d: ", i);
scanf("%d", p+i);
}
printf("\nFinal array: \n\n");
for(i = 0; i < 9; i++)
{
printf("%d ", *(p+i) );
}
// signal to operating system program ran fine
return 0;
}
预期输出:
Initial size of the array is 4
Enter element at index 0: 11
Enter element at index 1: 22
Enter element at index 2: 33
Enter element at index 3: 44
Increasing the size of the array by 5 elements ...
Enter 5 more integers
Enter element at index 4: 1
Enter element at index 5: 2
Enter element at index 6: 3
Enter element at index 7: 4
Enter element at index 8: 5
Final array:
11 22 33 44 1 2 3 4 5
字符串
C 语言中的字符串基础
最后更新于 2020 年 7 月 27 日
到目前为止,我们编写的程序只处理数字或字符,但一个真实世界的程序应该能够在需要时存储和操作文本。不幸的是,C 没有为字符串提供单独的数据类型,像 Java 和 C#这样的语言为字符串提供了单独的类型,但 C 却不是这样。在 C 中,字符串存储为以空字符结尾的字符数组。字符数组只有在最后一个元素是空字符('\0'
)时才是字符串。空字符是一个转义序列,就像\n
(换行符)、\t
(制表符)一样,ASCII 值为0
。例如:
char name[10] = {
's', 't', 'r', 'i', 'n', 'g' ,'\0'};
因此,我们可以说字符串只是一维字符数组,最后一个元素是空字符(“\0”)。
字符串文字
字符串文字只是用双引号(""
)括起来的一系列字符。它也被称为一个弦常量。以下是一些字符串的例子:
"I am learning C"
"My Lucky Number is 1"
"Hello World!"
""
双引号(""
)不是字符串文字的一部分,它们只是用来描述(即标记边界)一个字符串。每当您在程序中创建字符串文字时,编译器都会自动在末尾添加空字符('\0'
)。
字符串是如何存储的?
如上所述,字符串实际上是以空字符('\0'
)结尾的字符数组。每当编译器看到长度为n
的字符串时,它就为该字符串分配n + 1
个连续字节的内存。该内存将包含字符串中的所有字符,以及字符串末尾的空字符('\0'
)。因此字符串文字"Hello World"
将存储在内存中,如下所示:
如您所见,字符串"Hello World"
存储为一个12
字符数组(包括'\0'
)。
字符串也可以为空。
""
(空字符串,只包含'\0'
)。它将作为一组1
字符存储在内存中。
作为指针的字符串文字
字符串像数组一样存储。要理解的最重要的一点是,字符串文字是指向数组第一个字符的指针。换句话说"Hello World"
是指向字符'H'
的指针。由于"Hello World"
指向字符'H'
的地址,它的基本类型是指向char
或(char *)
的指针。这意味着,如果我们有一个指向char
或(char*)
类型的指针变量,我们可以将字符串指定为:
char *str = "Hello World";
在此赋值后str
指向第一个元素的地址,使用指针算法我们可以访问字符串文本中的任何字符。
printf("%c" ,*(str+0) ); // prints H
printf("%c" ,*(str+4) ); // prints o
即使您可以访问字符串文字的单个元素。试图修改字符串文字是一种未定义的行为,可能会导致程序崩溃。
*str = 'Y'; // wrong
由于"Hello World"
是指针,我们可以直接对其应用指针算法。例如:
"Hello World" + 0
指向字符'H'
的地址。
"Hello World" + 1
指向字符'e'
的地址。
"Hello World" + 2
指向字符'l'
的地址。
等等。
要获取地址"Hello World" + 1
处的值,只需取消引用表达式。
*("Hello World" + 1)
给出'e'
T4*("Hello World" + 2)
给出'l'
等等。
在第一章一维数组中,我们讨论了:
int arr[] = {
16,31,39,59,11};
那么写arr[i]
和写*(arr+i)
是一样的。
因此*("Hello World" + 1)
也可以写成"Hello World"[1]
。
重新审视 printf()和 scanf()
如果你看一下scanf()
和print()
的原型,你会发现这两个函数都期望一个类型为(char*)
的值作为它们的第一个参数。
int printf (const char*, ...);
int scanf (const char*, ...);
**注意:**暂时忽略关键字const
。这将在接下来的章节中详细讨论。
现在你知道当你调用printf()
函数时:
printf("Hello World");
您实际上是在传递"Hello World"
的地址,即指向数组第一个字母'H'
的指针。
字符串文字 v/s 字符文字
初学者经常混淆"a"
和'a'
,前者是字符串,其中"a"
是指向包含字符'a'
的内存位置的指针,后跟一个空字符('\0'
)。另一方面,字符文字,'a'
代表字符'a'
的 ASCII 值,即97
。因此,在需要字符串文字的地方千万不要使用字符文字,反之亦然。
多行字符串文字
您不限于单行字符串。如果您的字符串足够大,可以容纳在一行中,那么您可以通过在行尾添加反斜杠来扩展它。例如:
printf("This is first line \
some characters in the second line \
even more characters in the third line \n");
使用转义序列
您可以在字符串中使用像\n
(换行符)、\t
(制表符)这样的转义序列。例如:
printf("Lorem ipsum \ndolor sit \namet, consectetur \nadipisicing elit \nsed do eiusmod");
预期输出:
dolor sit
amet, consectetur
adipisicing elit
sed do eiusmod
字符串文字后跟字符串文字
当两个字符串文字彼此相邻时,编译器会将它们串联起来,并在串联字符串的末尾追加空字符(“\0”)。
print("Hello"" World"); // prints Hello World
和写作一样:
print("Hello World");
字符串变量
由于字符串是一个字符数组,我们必须声明一个足够大的数组来存储所有字符,包括空字符('\0'
)。
char ch_arr[6];
这里ch_arr
只能容纳6
个字符,包括空字符('\0'
)。如果在声明时初始化数组的元素,那么可以省略大小。
char ch_arr[] = {
'H', 'e', 'l', 'l', 'o', ' ', 'W', 'o', 'r', 'l', 'd'};
C 语言还为初始化字符串提供了一种更简洁、更容易键入的语法。例如,上述语句也可以写成:
char ch_arr[] = "Hello World";
我们已经研究过字符串文字是数组第一个字符的指针,但是这个规则有一个例外:当字符串文字被用来初始化一个字符数组,就像上面的语句一样,那么它不代表任何地址。这意味着我们不能使用"Hello World"
进行指针运算。数组ch_arr
的所有字符将存储在存储器中,如下所示:
如果要存储的字符数(包括'\0'
)小于数组的大小怎么办。在这种情况下,编译器会添加额外的空字符('\0'
)。例如:
char name[10] = "john";
数组name
将存储在存储器中,如下所示:
如果要存储的字符数(包括'\0'
)大于数组的大小,那么编译器会显示一条警告消息:数组初始值设定项中有多余的元素。
一般来说,创建字符串的最好方法是忽略数组的大小,在这种情况下,编译器会根据初始值设定项中的字符数来计算数组的大小。例如:
char str[] = "this is the best way";
需要注意的是,省略大小并不意味着数组字符串的长度可以在程序后期增加或减少(使用malloc()
或calloc()
来调整数组的长度)。一旦程序被编译,字符串的大小被固定为21
字节。由于对长字符串中的字符进行计数是一个容易出错的过程,因此当字符串太长时,此方法也是首选方法。
让我们通过创建两个简单的程序来结束这一章。
例 1:
下面的程序打印字符串的字符和每个字符的地址。
#include<stdio.h>
int main()
{
int i;
char str[6] = "hello";
for(i = 0; str[i] != '\0'; i++)
{
printf("Character = %c\t Address = %u\n", str[i], &str[i]);
}
// signal to operating system program ran fine
return 0;
}
预期输出:
Character = h Address = 2686752
Character = e Address = 2686753
Character = l Address = 2686754
Character = l Address = 2686755
Character = o Address = 2686756
**注意:**每次运行程序时,地址可能会有所不同。
在程序中需要注意的重要事情是 for 循环中的终止条件,它说:继续循环,直到遇到空字符。
例 2:
下面的程序使用指针打印字符串中的字符和字符的地址。
#include<stdio.h>
int main()
{
int i;
char str[6] = "hello";
char *p;
for(p = str; *p != '\0'; p++)
{
printf("Character = %c\t Address = %u\n", *(p), p);
}
// signal to operating system program ran fine
return 0;
}
预期输出:
Character = h Address = 2686752
Character = e Address = 2686753
Character = l Address = 2686754
Character = l Address = 2686755
Character = o Address = 2686756
**注意:**每次运行程序时,地址可能会有所不同。
工作原理:
这里,我们已经将数组名称字符串(指向char
或(char*)
的指针)分配给指针变量p
。这个说法之后p
和str
都指向了同一个同一个阵。现在我们可以使用指针算法来回移动来访问数组中的元素。对于循环的每次迭代,将p
的值增加1
。当p
指向空字符('\0'
)的地址时,for 循环停止。