友情链接:专栏地址
知识总结顺序参考C Primer Plus(第六版)和谭浩强老师的C程序设计(第五版)等,内容以书中为标准,同时参考其它各类书籍以及优质文章,以至减少知识点上的错误,同时方便本人的基础复习,也希望能帮助到大家
最好的好人,都是犯过错误的过来人;一个人往往因为有一点小小的缺点,将来会变得更好。如有错漏之处,敬请指正,有更好的方法,也希望不吝提出。最好的生活方式就是和努力的大家,一起奔跑在路上
文章目录
- 🚀指针与函数
- ⛳一、函数返回值使用指针
- ⛳二、指针变量作为函数参数
- 🎈(一)数组传参
- 🎈(二)指针传参
- ⛳三、函数指针
- 🎈1.声明函数指针
- 🎈2.用函数指针访问函数
- 🎈3.函数指针作为函数参数
- 🚀指针与数组
- ⛳一、指针表示法和数组表示法
- ⛳二、指针和多维数组
- ⛳三、指针数组
- ⛳四、数组指针
- ⛳五、数组和指针的联系
🚀指针与函数
⛳一、函数返回值使用指针
可以返回函数内部:
- 动态分配内存地址
- 局部静态变量地址
- 全局静态变量和外部变量地址
// demo 9-8.c
#include <stdio.h>
int * add(int x, int y)
{
int sum = x + y;
return ∑
}
//返回动态内存分配地址
int * add1(int x, int y)
{
int * sum = NULL;
sum = (int*)malloc(sizeof(int));
*sum = x + y;
return sum;
}
//返回局部静态变量的地址
int * add2(int x, int y)
{
static int sum = 0;
printf("sum: %d\n", sum);
sum = x + y;
return ∑
}
int main()
{
int a = 3, b = 5;
int *sum = NULL;
//不能使用外部函数局部变量的地址 bad
sum = add(a, b);
//接收外部函数动态内存分配的地址 ok
sum = add1(a, b);
//接收外部函数局部静态变量的地址
sum = add2(a, b);
*sum = 88888;
add2(a, b);
system("pause");
return 0;
}
函数的调用可以(而且只可以)得到一个返回值(即函数值),而使用指针变量作参数,可以得到多个变化了的值。如果不用指针变量是难以做到这一点的。要善于利用指针法。
⛳二、指针变量作为函数参数
函数的参数不仅可以是整型、浮点型、字符型等数据,还可以是指针类型。它的作用是将一个变量的地址传送到另一个函数中。
编写一个处理基本类型(如,int)的函数时,要选择是传递int类型的值还是传递指向int的指针。通常都是直接传递数值,只有程序需要在函数中改变该数值时,才会传递指针。
🎈(一)数组传参
1.数组传参时,会退化为指针!
- C 语言只会以值拷贝的方式传递参数,参数传递时,如果拷贝整个数组,效率会大大降低,并且在参数位于栈上,太大的数组拷贝将会导致栈溢出。
- 因此,C 语言将数组的传参进行了退化。将整个数组拷贝一份传入函数时,将数组名看做常量指针,传数组首元素的地址
对于数组别无选择,必须传递指针,因为这样做效率高。如果一个函数按值传递数组,则必须分配足够的空间来储存原数组的副本,然后把原数组所有的数据拷贝至新的数组中。如果把数组的地址传递给函数,让函数直接处理原数组则效率要高。
传递地址会导致一些问题。C 通常都按值传递数据,因为这样做可以保证数据的完整性。如果函数使用的是原始数据的副本,就不会意外修改原始数据。但是,处理数组的函数通常都需要使用原始数据,因此这样的函数可以修改原数组。有时,这正是我们需要的。例如,下面的函数给数组的每个元素都加上一个相同的值:
void add_to(double arr[], int n, double val) { int i; for (i = 0; i < n; i++) arr[i] += val; } //调用该函数后,prices数组中的每个元素的值都增加了2.5: add_to(prices, 100, 2.50);
该函数修改了数组中的数据。之所以可以这样做,是因为函数通过指针直接使用了原始数据。
2.用数组的形式传递参数,不需要指定参数的大小, 因为在一维数组传参时,形参不会真实的创建数组, 传的只是数组首元素的地址。当然:写上一个数组大小也是可以的,方便自己知道这个数组有多大,在函数代码中也写上固定的数组大小,其它并无多大用处,常用法还是将数组原大小新增一个参数传递进来
void method_2(int arr[10])
{
for(int i=0; i<10; i++){
printf(" arr[%d] = %d\n", i, arr[i]);
}
}
method_2(arr);
3.既然退化为指针!,可以直接使用指针形式传参,用指针进行接收,传的是数组首元素的地址,这里就必须单独添加一个参数表明待处理数组的元素个数
void method_3(int *arr, int len)
{
for(int i=0; i<len; i++){
printf(" arr[%d] = %d\n", i, arr[i]);
}
}
method_3(arr, 10);
对形式参数使用const:
在K&R C的年代,避免以上提到的误传递指针修改原始数据的唯一方法是提高警惕。ANSI C提供 了一种预防手段。如果函数的意图不是修改数组中的数据内容,那么在函数原型和函数定义中声明形式参数时应使用关键字const。例如:
int sum(const int ar[], int n); /* 函数原型 */
int sum(const int ar[], int n) /* 函数定义 */
{
int i;
int total = 0;
for( i = 0; i < n; i++)
total += ar[i];
return total;
}
以上代码中的const告诉编译器,该函数不能修改ar指向的数组中的内容。如果在函数中不小心使用类似ar[i]++的表达式,编译器会捕获这个错误,并生成一条错误信息。
这样使用const并不是要求原数组是常量,而是该函数在处理数组时将其视为常量,不可更改。这样使用const可以保护数组的数据不被修改,就像按值传递可以保护基本数据类型的原始值不被改变一 样。一般而言,如果编写的函数需要修改数组,在声明数组形参时则不使用 const;如果编写的函数不用修改数组,那么在声明数组形参时最好使用 const。
🎈(二)指针传参
如果不是数组,是其它普通类型,需要修改原数据的值,我们需要使用指针参数:
int main(void) {
int a = 1;
int *p = &a;
void setnum(int* c) {
scanf("%d",c); //这里a就是地址,不用再加&
}
//调用该函数:
setnum(&a);
printf("%d",a);
setnum(p); //p的值也是&a
printf("%d",a);
return 0;
}
-
如果需要修改原数据的值,需要使用指针作为参数,同时,调用函数时的实参应该是需要修改的值的地址,要加上&
-
当我们使用指针作为参数,也可以单纯传递一个指针,毕竟指针就是地址,就不需要使用&,这两种情况本质是一样的
-
函数要处理数组必须知道何时开始、何时结束。一般用一个整数形参表明待处理数组的元素个数,但是这并不是给函数传递必备信息的唯一方法。还有一种方法是传递两个指针,第1个指针指明数组的开始处(与前面用法相同),第2个指针指明数组的结束处。
int sump(int * start, int * end); int marbles[SIZE] = { 20, 10, 5, 39, 4, 16, 19, 26,31, 20 }; long answer; answer = sump(marbles, marbles + SIZE);
⛳三、函数指针
假设有一个指向 int类型变量的指针,该指针储存着这个int类型变量储存在内存位置的地址。 同样,函数也有地址,因为函数的机器语言实现由载入内存的代码组成。指向函数的指针中储存着函数代码的起始处的地址。
🎈1.声明函数指针
声明一个数据指针时,必须声明指针所指向的数据类型。声明一个函数指针时,必须声明指针指向的函数类型。为了指明函数类型,要指明函数签名,即函数的返回类型和形参类型。
void ToUpper(char *); // 把字符串中的字符转换成大写字符
//ToUpper()函数的类型是“带char * 类型参数、返回类型是void的函数”。 下面声明了一个指针pf指向该函数类型:
void (*pf)(char *); // pf 是一个指向函数的指针
第1对圆括号把*和pf括起来,表明pf是一个指向函数的指针。因此,(*pf)是一个参数列表为(char *)、返回类型为void的函数。
创建函数指针最简单的方法把函数声明移过来,把函数名改成 (* 函数指针名),即先写出该函数的原型,后把函数名替换成(*pf)形式的表达式,
🎈2.用函数指针访问函数
既然可以用数据指针访问数据,也可以用函数指针访问函数。奇怪的是,有两种逻辑上不一致的语法可以这样做,
void ToUpper(char *);
void ToLower(char *);
void (*pf)(char *);
char mis[] = "Nina Metier";
pf = ToUpper;
(*pf)(mis); // 把ToUpper 作用于(语法1)
pf = ToLower;
pf(mis); // 把ToLower 作用于(语法2)
-
第1种方法:由于pf指向ToUpper 函数,那么*pf就相当于ToUpper函数,所以表达式(*pf)(mis)和ToUpper(mis) 相同。
-
第2种方法:由于函数名是指针,那么指针和函数名可以互换使用,所以pf(mis) 和ToUpper(mis)相同。从pf的赋值表达式语句就能看出ToUpper和pf是等价的。
拓展:
由于历史的原因,贝尔实验室的C和UNIX的开发者采用第1种形式,而 伯克利的UNIX推广者却采用第2种形式。K&R C不允许第2种形式。但是, 为了与现有代码兼容,ANSI C认为这两种形式(本例中是(*pf)(mis)和 pf(mis))等价。
🎈3.函数指针作为函数参数
作为函数的参数是数据指针最常见的用法之一,函数指针亦如此,告诉该函数要使用哪一个函数,例如:
void show(void (* fp)(char *), char * str);
-
它声明了两个形参:fp和str。fp形参是一个函数指针,str是一个数据指针。
-
可以这样调用函数:
show(ToLower, mis); /* show()使用ToLower()函数:fp = ToLower */ show(pf, mis); /* show()使用pf指向的函数: fp = pf */
-
把带返回值的函数作为参数传递给另一个函数有两种不同的方法。
function1(sqrt); /* 传递sqrt()函数的地址 */ function2(sqrt(4.0)); /* 传递sqrt()函数的返回值 */
第1条语句传递的是sqrt()函数的地址,假设function1()在其代码中会使用该函数。第2条语句先调用sqrt()函数,然后求值,并把返回值(该例中是 2.0)传递给function2()。
🚀指针与数组
⛳一、指针表示法和数组表示法
从以上分析可知,处理数组的函数实际上用指针作为参数,但是在编写这样的函数时,可以选择是使用数组表示法还是指针表示法
数组完全可以使用指针来访问, days[i] 和 *(days+i) 这两个表达式是等价的,无论 days 是数组名还是指针变量,这两个表达式都没问题。但是,只有当 days 是指针变量时,才能使用ar++这样的表达式。
指针表示法(尤其与递增运算符一起使用时)更接近机器语言,因此一 些编译器在编译时能生成效率更高的代码。然而,许多程序员认为他们的主要任务是确保代码正确、逻辑清晰,而代码优化应该留给编译器去做。
⛳二、指针和多维数组
在学习指针数组和数组指针之前,有必要了解指针和多维数组之间的关系,例如:
int zippo[4][2]; /* 内含int数组的数组 */
-
同样,数组名zippo是该数组首元素的地址。zippo的首元素是一个内含两个int值的数组,所以zippo是这个内含两个int值的数组的地址。
-
因为zippo是数组首元素的地址,所以zippo的值和&zippo[0]的值相同。 而zippo[0]本身是一个内含两个整数的数组,所以zippo[0]的值和它首元素 (一个整数)的地址(即&zippo[0][0]的值)相同。简而言之,zippo[0]是一个占用一个int大小对象的地址,而zippo是一个占用两个int大小对象的地址。
-
给指针或地址加1,其值会增加对应类型大小的数值。在这方面,zippo 和zippo[0]不同,因为zippo指向的对象占用了两个int大小,而zippo[0]指向的对象只占用一个int大小。因此, zippo + 1和zippo[0] + 1的值不同。
-
解引用一个指针(在指针前使用*运算符)或在数组名后使用带下标的 []运算符,得到引用对象代表的值。
- 因为zippo[0]是该数组首元素(zippo[0] [0])的地址,所以*(zippo[0])表示储存在zippo[0][0]上的值(即一个int类型的值)。
- *zippo代表该数组首元素(zippo[0])的值,但是 zippo[0]本身是一个int类型值的地址。该值的地址是&zippo[0][0],所以 *zippo就是&zippo[0][0]。
- **zippo与 *&zippo[0][0]等价,这相当于zippo[0][0],即一个int类型的值。简而言之, zippo是地址的地址,必须解引用两次才能获得原始值。
-
特别注意,与 zippo[2][1]等价的指针表示法是*(*(zippo+2) + 1)。
如果程序恰巧使用一个指向二维数组的指针,而且要通过该指针获取值时,最好用简单的数组表示法,而不是指针表示法。
⛳三、指针数组
存储指针的数组叫指针数组,之前已经讲过,用双引号括起来的内容被视为指向该字符串储存位置的指针,同时为了增加对此概念的熟悉,这里我们就也用字符串来讲解指针数组
定义:
类型 *指针数组名[元素个数]
//例如:定义一个有两个元素的指针数组,每个元素都是一个int类型指针变量
int *qishou[2];
字符串数组:
#define SLEN 40
#define LIM 5
//mytalents数组是一个内含5个指针的数组,共占用40字节
const char *mytalents[LIM] = {
"Adding numbers swiftly",
"Multiplying accurately", "Stashing data",
"Following instructions to the letter",
"Understanding the C language"
};
//yourtalents是一个内含5个数组的数组,每个数组内含40个char类型的值,共占用200字节。
char yourtalents[LIM][SLEN] = {
"Walking in a straight line",
"Sleeping", "Watching television",
"Mailing letters", "Reading email"
};
- 使用一个下标时都分别表示一个字符串,如mytalents[0]和 yourtalents[0];使用两个下标时都分别表示一个字符,例如 mytalents[1][2]表示 mytalents 数组中第 2 个指针所指向的字符串的第 3 个字符’l’, yourtalents[1][2]表示youttalentes数组的第2个字符串的第3个字符’e’。
- 虽然mytalents[0]和 yourtalents[0]都分别表示一个字符串,但mytalents和yourtalents的类型并不相同。mytalents中的指针指向初始化时所用的字符串常量的位置,这些字符串字面量被储存在静态内存中;而 yourtalents 中的数组则储存着字符串字面量的副本,所以每个字符串都被储存了两次。
- 如果要用数组表示一系列待显示的字符串,请使用指针数组,因为它比二维字符数组的效率高。
⛳四、数组指针
指向数组的指针叫做数组指针,
定义:
类型 (*数组指针名)[数组元素个数]
//例如:定义一个指向三个成员的数组的指针
int (*p)[3];
使用圆括号,因为[]的优先级高于*,必须用个圆括号将*p单独括起来,*先与pz结合,因此声明的是一个指向数组(内含两个int类型的
值)的指针不然就变成了指针数组
#include <stdio.h>
int main(void)
{
int zippo[4][2] = { { 2, 4 }, { 6, 8 }, { 1, 3 },{ 5, 7 } };
int(*pz)[2];
pz = zippo; //p = &zippo[0]两者是一样的
printf(" pz = %p, pz + 1 = %p\n", pz, pz + 1);
printf("pz[0] = %p, pz[0] + 1 = %p\n", pz[0], pz[0] + 1);
printf(" *pz = %p, *pz + 1 = %p\n", *pz, *pz + 1);
printf("pz[0][0] = %d\n", pz[0][0]);
printf(" *pz[0] = %d\n", *pz[0]);
printf(" **pz = %d\n", **pz);
printf(" pz[2][1] = %d\n", pz[2][1]);
printf("*(*(pz+2) + 1) = %d\n", *(*(pz + 2) + 1));
printf("%d\n",(*(pz+1))[1]);
printf("%d",*(*(pz + 1) + 1));
return 0;
}
下面是该程序的输出:
pz = 0x0064fd38, pz + 1 = 0x0064fd40
pz[0] = 0x0064fd38, pz[0] + 1 = 0x0064fd3c
*pz = 0x0064fd38, *pz + 1 = 0x0064fd3c
pz[0][0] = 2
*pz[0] = 2
**pz = 2
pz[2][1] = 3
*(*(pz+2) + 1) = 3
8
8
虽然p是一个指针,不是数组名,但是也可以使用p[2][1]这样的写法。可以用数组表示法或指针表示法来表示一个数组元素,既可以使用数组名,也可以使用指针名:
-
数组法:
zippo[m][n] == (*(p+m))[n];
-
指针法:
zippo[m][n] == *(*(p + m) + n)
⛳五、数组和指针的联系
初始化字符数组来储存字符串和初始化指针来指向字符串有何区别 (“指向字符串”的意思是指向字符串的首字符)?
同样,C语言字符串也是通过字符数组来实现的,这里也正好用字符数组来总结以下指针和数组的相关联系:能使用指针表示数组名,也可以用数组名表示指针
讲解代码:
const char * pt1 = "Something is pointing at me."; //共29个字符,注意还有一个'\0'
//等同于:
const char ar1[] = "Something is pointing at me.";
1.数组形式(ar1[])
-
在计算机的内存中分配为一个内含29个元素的数组(每个元素对应一个字符,还加上一个末尾的空字符’\0’)
-
每个元素被初始化为字符串字面量对应的字符。
-
通常,字符串都作为可执行文件的一部分储存在数据段中。当把程序载入内存时,也载入了程序中的字符串。字符串储存在静态存储区 (static memory)中。但是,程序在开始运行时才会为该数组分配内存。此时,才将字符串拷贝到数组中。注意,此时字符串有两个副本。一个是在静态内存中的字符串常量,另一个是储存在ar1数组中的字符串。
-
此后,编译器便把数组名ar1识别为该数组首元素地址(&ar1[0])的别名。
在数组形式中,ar1是地址常量。不能更改ar1,如果改变了ar1,则意味着改变了数组的存储位置(即地址)。可以进行类似 ar1+1这样的操作,标识数组的下一个元素。但是不允许进行++ar1这样的操 作。递增运算符只能用于变量名前(或概括地说,只能用于可修改的左 值),不能用于常量。
*2.指针形式(pt1)
-
指针形式(*pt1)也使得编译器为字符串在静态存储区预留29个元素的空间。
-
另外,一旦开始执行程序,它会为指针变量pt1留出一个储存位置, 并把字符串的地址储存在指针变量中。
该变量最初指向该字符串的首字符, 但是它的值可以改变。因此,可以使用递增运算符。例如,++pt1将指向第 2 个字符(o)。
3.const
字符串常量被视为const数据。由于pt1指向这个const数据,所以应该把pt1声明为指向const数据的指针,即常量指针。这意味着不能用pt1改变它所指向的数据,但是仍然可以改变pt1的值(即,pt1指向的位置)
如果把一个字符串常量拷贝给一个数组,就可以随意改变数据,除非把数组声明为const。
总之,初始化数组把静态存储区的字符串拷贝到数组中,而初始化指针只把字符串的地址拷贝给指针。
4.区别
假设有下面两个声明:
char heart[] = "I love Tillie!";
const char *head = "I love Millie!";
-
首先,两者都可以使用数组表示法
for (i = 0; i < 6; i++) putchar(heart[i]); putchar('\n'); for (i = 0; i < 6; i++) putchar(head[i]); putchar('\n');
-
其次,两者都能进行指针加法操作(指针表示法):
for (i = 0; i < 6; i++) putchar(*(heart + i)); putchar('\n'); for (i = 0; i < 6; i++) putchar(*(head + i)); putchar('\n');
-
不能使用指针修改字符串
char * p1 = "Klingon"; p1[0] = 'F'; // ok? printf("Klingon"); printf("%s",p1);
编译器可以使用内存中的一个副本来表示所有完全相同的字符串常量。char *字符指针指向的数据存储在静态存储区,里面的值不允许修改,相当于const char *
如果要修改,改成用非const数组初始化为字符串常量即可:char p1[] = “Klingon”;
实际上在过去,一些编译器由于这方面的原因,其行为难以捉摸,而另一些编译器则导致程序异常中断。因此,建议在把指针初始化为字符串字面量时使用const限定符:
const char * pl = "Klingon"; // 推荐用法