c语言基础学习07
=============================================================================
涉及到的知识点有:
1、指针、指针的概念、指针变量的定义、取地址运算符 &、无类型指针、
指针占用内存的说明、野指针 与 空指针、空指针理解的扩展、指针的兼容性(即指针类型之间一定要匹配)、
不同的数据类型在内存中占用的地址、指向常量的指针 和 指针常量、指针与数组的关系、指针运算、
通过指针使用数组元素、不同类型的指针的区别以及与数组的关系、小案例:int类型与ip地址的对应关系
使用指针给二维数组排序、
2、指针数组、二级指针(指向指针的指针)、三级指针及其以上指针、函数的参数为指针变量时(指针变量作为函数的参数)、
函数的参数为数组名时(即数组名作为函数的参数)、函数的返回值为指针时(即指针作为函数的返回值)、
几个c语言的库函数:memset、memcpy、memmove函数,使用的时候需要包含头文件 #include <string.h>、
3、字符指针 与 字符串、通过指针访问字符串数组、通过指针使得字符串逆置、函数的参数为char *(即char *作为函数的参数)、
自定义函数实现求字符串长度和字符串拷贝、例外:如果函数的参数是一个字符串时,那么并不需要再传递一个参数说明这个字符串有多长、
4、指针数组作为main函数的形参、举个小例子:用到main函数的参数,实现计算两个数的和、
课后作业写一个程序,需要用到main函数的参数、
=============================================================================
c语言是面向过程的语言,是弱类型语言,c语言的源代码基本就是无数个函数的堆砌。
即很多函数就组成c语言源代码了,也即它的源代码基本就是函数构成的。
C语言里面的test()和test(void)是不一样的。什么也不写的话,C语言就比较含糊了,容易出错,结果不可知。
C++语言里面的test()和test(void)是一样的。
c语言几个松散的地方(不足的地方,不严禁的地方,它容易出错的地方)。
课后思考:
写一个函数求字符串的长度。课后思考,用递归函数实现求字符串长度。
=============================================================================
指针
指针是c语言里面最抽象的、最重要的、最常用的。
指针的概念:
指针变量也是一个变量,
指针存放的内容是一个地址,该地址指向一块内存空间,
指针是一种数据类型(指针类型)。
--------------------------------------
计算机内存的最小单位是什么?BYTE(字节)
对于内存,每个BYTE(字节)都有一个唯一不同的编号,这个编号就是内存地址。
操作系统就给内存的每一个字节编了一个号,所以说:一个编号对应的是一个BYTE(字节)的空间大小。
打比方:
1 -> BYTE
2 -> BYTE
3 -> BYTE
4 -> BYTE
对应于
--------------------------------------
一个int多大?答:4个BYTE(字节),所以一个int占用了了4个编号(即4个不同的内存地址)。
地址的编号:在32位系统下是一个4个字节的无符号整数;在64位系统下是一个8个字节的无符号整数。
(因为地址不可能是负的,又因为无符号数可以表达一个更大的地址,有符号数表示的最大地址会变小)
-----------------------------------------------------------------------------
指针变量的定义:
可以定义一个指向一个变量的指针变量。
-----------------------------------------------------------------------------
取地址运算符 &
& 可以取得一个变量在内存当中的地址。(取地址取的是内存地址)
register int a; //寄存器变量,这种变量不在内存里面,而在cpu里面,所以是没有地址的,
所以寄存器变量不能使用&来得到地址。
-----------------------------------------------------------------------------
无类型指针
定义一个指针变量,但不指定它指向具体哪种数据类型。可以通过强制转化将 void * 转化为其他类型指针,
也可以用 (void *) 将其他类型强制转化为void类型指针。
void *p; 指针之间赋值需要类型相同,但任何类型的指针都可以赋值给 void * 。
-----------------------------------------------------------------------------
linux下示例代码如下: int main()
{
int *p; //定义了一个可以指向int类型地址的指针变量,指针变量的名字叫p。*不是指针变量名字的一部分。
//int * 是一种数据类型。
int a; //定义了一个int类型的变量,int变量的名字叫a。 a = ; //int * 和 int是两种不同的数据类型。
p = &a; //把a的内存地址赋值给指针变量p。 printf("%p\n", p); //0x7fff5b2faedc 输出的是a的首地址的编号,不会把四个编号都输出的。
//而且注意:每一次执行该代码后,输出的编号都会发生变化! *p = ; //通过指针变量间接的访问a的值,*p代表指针指向变量的值,p代表指向变量的地址。
printf("a = %d\n", a); //a = 10; 通过上面的方法把a的值改变了。 a = ;
printf("%d\n", *p); //100 通过指针变量间接的访问a的值。 int b = ;
p = &b; //又把b的内存地址赋值给p。
*p = ;
printf("b = %d\n", b); //20 //char *p1 = &a; //相当于 char *p1; p1 = &a;//两个类型不相同的地址。即指针类型不兼容。那么我们强转试试!
char *p1 = (char *)&a;
a = ;
*p1 = ;
printf("a = %d\n", a); //a = 123392 就算强转后也会出现问题,所以要避免指针类型不兼容问题。 void *p2; //可以指向任何类型的地址,void代表无类型。 return ;
}
-----------------------------------------------------------------------------
指针占用内存的说明
在同一个系统下,不管指针指向什么样类型的变量,地址的大小(或叫编号的大小)总是一样的。
linux下示例代码如下: int main()
{
char *p1;
int *p2;
long long *p3; printf("%lu, %lu, %lu\n", sizeof(p1), sizeof(p2), sizeof(p3)); //实质是:编号的大小是多少?
return ; //输出的是 8, 8, 8
//地址的编号:在32位系统下是一个4个字节的无符号整数;在64位系统下是一个8个字节的无符号整数。
//指针变量的名字叫p1、p2、p3。指针变量的大小是多大呢?因为指针变量对应的是某某的首地址的编号,
//即指针变量对应的是编号,而编号就是内存地址。即编号在64位系统下是一个8个字节的无符号整数。
//所以指针变量的大小就是编号的大小,而编号在64位系统下用8个字节的无符号整数表示。
//举例子说明下:同一个酒店,房间的编号的长度都是一样的。 }
--------------------------------------
再比如:
linux下示例代码如下: #include <stdio.h> int main()
{
int *p1;
int a = ;
p1 = &a;
*p1 = ;
//p1 = 10; int *p2;
p2 = &a;
//*p2是什么?不管是*p1还是*p2都代表变量a的值,但p1和p2确实是两个不同的指针变量。
return ;
}
画图说明如下:
=============================================================================
野指针 与 空指针
野指针:没有指向任何有效地址的指针变量,所以在代码中避免出现野指针,
如果一个指针不能确定指向任何一个变量地址,那么就将这个指针变成空指针。
linux下示例代码如下: #include <stdio.h> int main()
{
int *p;
*p = ; //不能这样写,没有初始化过值的指针,这种指针叫野指针。
return ; //因为地址编号所占用的内存不是你程序要调用的内存。对于操作系统而言,不是你的内存你就不能改!
//如果你非要改的话,操作系统就会发现你在做非法操作,会直接把你清理出去了。即程序出错。
}
编译上段程序没有错误,运行上段程序会出现一个错误:Segmentation fault(段错误,也即分段故障)
-----------------------------------------------------------------------------
空指针:就是指向了NULL的指针变量。
linux下示例代码如下: #include <stdio.h> int main()
{
int *p; //两句代码相当于一句:int *p = NULL;
p = NULL; //如果一个指针变量没有明确的指向一块内存,那么就把这个指针变量指向NULL。
//这个指针就是空指针,空指针是合法的。
//实际上NULL并不是c语言的关键字,NULL在c语言中的定义是:#define NULL 0
//NULL在c语言里面就是一个宏常量,值是0。那么我们为什么不直接写0呢?
//NULL代表的是空指针,而不是一个整数零,这样看的会舒服些。(这只是粗浅易懂的解释)
return ;
}
程序中不要出现野指针,但可以出现空指针。
--------------------------------------
空指针理解的扩展:
注意:
int a = ;
int *p = &a; //相当于 int *p; p = &a; int *node = NULL; //相当于:int *node; node = NULL;
NULL就是系统定义特殊的0,把你初始化的指针指向它,可以防止“野指针”的恶果。
NULL是个好东西,给一出生的指针一个安分的家。
--------------------------------------
用C语言编程不能不说指针,说道指针又不能不提NULL,那么NULL究竟是个什么东西呢? C语言中又定义,定义如下:
1 #undef NULL
#if defined(__cplusplus)
#define NULL 0
#else
#define NULL ((void *)0)
#endif
所以我觉得,如果一个指针被赋予NULL,应该就相当于这个指针执行了0x0000这个逻辑地址,
但是C语言中0x0000这个逻辑地址用户是不能使用的,
(有些人说是因为0x0000没有映射到物理地址,也有人说是因为0x0000映射到的地址是操作系统用于判断野指针的,我也不太懂,总之就是用户不能使用啦)
所以当你试图取一个指向了NULL的指针的内容(或者叫值)时,就会提示段错误,听着有点绕,看程序:
int *node = NULL;
int a = ;
a = *node; //*node的意思是:取指针变量node的值。然后赋值给a。 printf("%d\n", a);
*node的意思是:取指针变量node的值,也就是逻辑地址0x0000,而这个地址是不能被访问的(即不能被取出来的),
c语言语法上没有问题,所以编译器编译没有问题,但是编译器编译后运行会出现段错误。
linux下示例代码如下: #include <stdio.h> int main()
{
int *p = NULL; //相当于 int *p = 0; 但一般不这么写啊!
int a = ;
a = *p;
printf("%d\n", a); return ;
}
root@iZ2zeeailqvwws5dcuivdbZ:~///指针# gcc -o p6 p6.c
root@iZ2zeeailqvwws5dcuivdbZ:~///指针# p6
Segmentation fault(段错误,也即分段故障)
root@iZ2zeeailqvwws5dcuivdbZ:~///指针#
=============================================================================
指针的兼容性(即指针类型之间一定要匹配)
指针之间赋值比普通数据类型赋值检查更为严格,例如:不可以把一个 double * 赋值给int。
#include <stdio.h> int main()
{
int *p;
char b = ;
p = &b; //指针类型之间一定要匹配,不然会有警告,强行运行的话,结果不可控! return ;
}
警告如下:
warning: assignment from incompatible pointer type [-Wincompatible-pointer-types]
警告:不兼容的指针类型分配[-Wincompatible-pointer-types]
=============================================================================
我们不要把指针想象的特别神秘!其实指针变量也是一个变量。
它里面放的就是一个地址的编号,地址的编号就是一个8个字节的无符号的整数(64位系统下)。
区别是:这个整数不能直接赋值,而是来自于对另外一个变量的取地址操作而得到!
=============================================================================
不同的数据类型在内存中占用的地址
我们先看几个现象:
linux下示例代码如下: #include <stdio.h> int main()
{
char a[];
printf("%p, %p, %p, %p\n", a, &a[], &a[], &a[]);
return ;
}
root@iZ2zeeailqvwws5dcuivdbZ:~///指针# gcc -o p8 p8.c
root@iZ2zeeailqvwws5dcuivdbZ:~///指针# p8
0x7ffe4f449e90, 0x7ffe4f449e90, 0x7ffe4f449e91, 0x7ffe4f449e92
root@iZ2zeeailqvwws5dcuivdbZ:~///指针# gcc -o p8 p8.c
root@iZ2zeeailqvwws5dcuivdbZ:~///指针# p8
0x7ffdae4e3c20, 0x7ffdae4e3c20, 0x7ffdae4e3c21, 0x7ffdae4e3c22
root@iZ2zeeailqvwws5dcuivdbZ:~///指针# gcc -o p8 p8.c
root@iZ2zeeailqvwws5dcuivdbZ:~///指针# p8
0x7ffd37b6f530, 0x7ffd37b6f530, 0x7ffd37b6f531, 0x7ffd37b6f532
root@iZ2zeeailqvwws5dcuivdbZ:~///指针#
每一次编译后执行,输出的地址会发生变化,但是相邻地址间的间隔不变。
--------------------------------------
再比如:
linux下示例代码如下: #include <stdio.h> int main()
{
int a[];
printf("%p, %p, %p, %p\n", a, &a[], &a[], &a[]);
return ;
} root@iZ2zeeailqvwws5dcuivdbZ:~///指针# gcc -o p8 p8.c
root@iZ2zeeailqvwws5dcuivdbZ:~///指针# p8
0x7ffe9e845ec0, 0x7ffe9e845ec0, 0x7ffe9e845ec4, 0x7ffe9e845ec8
root@iZ2zeeailqvwws5dcuivdbZ:~///指针# gcc -o p8 p8.c
root@iZ2zeeailqvwws5dcuivdbZ:~///指针# p8
0x7ffed37c6bc0, 0x7ffed37c6bc0, 0x7ffed37c6bc4, 0x7ffed37c6bc8
root@iZ2zeeailqvwws5dcuivdbZ:~///指针# gcc -o p8 p8.c
root@iZ2zeeailqvwws5dcuivdbZ:~///指针# p8
0x7ffdb8b422c0, 0x7ffdb8b422c0, 0x7ffdb8b422c4, 0x7ffdb8b422c8
root@iZ2zeeailqvwws5dcuivdbZ:~///指针#
每一次编译后执行,输出的地址会发生变化,但是相邻地址间的间隔不变。
其余的类型就不一一举例啦!
=============================================================================
指向常量的指针 和 指针常量
const int *p; //定义一个指向常量的指针。
int *const p; //定义一个指针常量,一旦指向某一变量的地址后,不可再指向其他变量的地址。(注意:指针常量也叫常量指针)
二者区别:
const int *p; //p是一个变量,但指向一个常量。(即p可以指向任何地址,但是只能通过*p来读这块地址的内容,不能通过*p来写这块地址的内容)
int *const p; //p是一个常量,但指向一个变量或者常量。(即如果一旦p指向了任何一个有效的地址后,就不可再指向其他变量的地址,但可以通过*p来读写这块地址的内容)
--------------------------------------
linux下示例代码如下: #include <stdio.h> int main01()
{
int a = ;
int *p = &a; //此时的p指向了一个int类型的地址,可以通过*p的方式来修改这个内存a的值。
*p = ;
printf("a = %d\n", *p); //或者printf("a = %d\n", a); //此时的*p可读可写。 return ;
} int main()
{
int a = ;
const int *p = &a; //此时的p指向了一个int类型的地址,但不可以通过*p的方式来修改这个内存a的值。
//*p = 10;
a = ; //但是呢,不可以通过*p来改a的值,可以通过a去修改a的值。
printf("a = %d\n", *p); //或者printf("a = %d\n", a); //此时的*p可读不可写。 //c语言的一个小漏洞
const int b = ;
//b = 0; //定义了一个常量,那么这个常量权限是只读了。 //通过指针的方法:即可以通过指向一个变量地址的指针去指向它,然后通过*p1去间接的修改b的值。
//注意编译的时候会出现警告!我们忽略这个警告强行改!这时把b的值改了!!!
//warning: assignment discards ‘const’ qualifier from pointer target type [-Wdiscarded-qualifiers]
//警告:赋值时从指针目标类型丢弃“const”限定符[-Wdiscarded-qualifiers] //这就是在c语言中用常量的时候不用const了!
//因为c语言中的const是有问题的,因为可以通过指针变量间接的修改const定义的常量的值,所以在c语言中用#define定义常量的时候更多。 //为什么#define不能改呢?实质上#define就是一个文本替换,直接把它替换成一个整数了,整数又不是一个变量。
//但是在C++中就没有这个漏洞了。为什么呢?因为c++里面的const是个真的const,而c语言中的const只是在语法的角度不让你去赋值,实际上是假的。
//这是c语言本身存在的弱项。 int *p1;
p1 = &b; //为了避免这个warning,使用强转即可:p1 = (int *)&b;
*p1 = ;
printf("b = %d\n", b); //或者printf("b = %d\n", *p); int *const p2 = &a; //表示p2指向了a的地址,而且p2只能指向a的地址,不可再指向其他变量的地址。
//p2 = &b;//直接编译错误//p2是一个指针常量,p2只能指向固定的一个变量的地址,但可以用*p2读写这个变量的值。 return ;
}
=============================================================================
指针与数组的关系
linux下示例代码如下: #include <stdio.h> int main()
{
int a[] = { , , , , , , , , , }; //数组的名字a就是数组首元素的地址。
int *p;
p = a; //当指针变量指向一个数组的时候,c语言规定指针变量名可以当做数组名使用。
//p = a[0]; //二者等价 a[] = ;
p[] = ; //相当于a[3] = 100;但二者之间也有区别哦!区别如下:
printf("%lu, %lu\n", sizeof(a), sizeof(p));//40, 8 二者所占内存大小不一样。 int i;
for (i = ; i <; i++)
{
//printf("a[%d] = %d\n", i, a[i]); //输出没有问题
printf("a[%d] = %d\n", i, p[i]); //输出也没有问题,完全把指针当数组用!
} return ;
}
--------------------------------------
一级指针画图小说明如下:
=============================================================================
指针运算
指针变量可以进行计算,如果是 int * 类型每加一,变化4个整数;
如果是 char * 类型每加一,变化1个整数。其他类型以此类推。
linux下示例代码如下:
#include <stdio.h> int main()
{
int a = ;
int *p = &a;
printf("%p, %p, %p\n", p, p + , p + ); //0x7fff5c2a518c, 0x7fff5c2a5190, 0x7fff5c2a5194 return ;
}
-----------------------------------------------------------------------------
指针运算小例子:
linux下示例代码如下:
#include <stdio.h> int main()
{
int a[] = { };
int *p = a; p += ;
*p = ; p -=;
*p = ; int i;
for (i = ; i < ; i++)
{
printf("a[%d] = %d\n", i, a[i]);
} return ;
}
输出的是:
a[] =
a[] =
a[] =
a[] =
a[] =
a[] =
a[] =
a[] =
a[] =
a[] =
-----------------------------------------------------------------------------
通过指针使用数组元素
linux下示例代码如下:
#include <stdio.h> int main()
{
int a[] = { , , , , , , , , , };
int *p = a; //指针p指向a的首地址。
p[] = ; //等价于 *(p + 3) = 100; 一般写成左边那样。 printf("a[%d] = %d\n", i, a[i]);
int i;
for (i = ; i < ; i++)
{
} return ;
}
=============================================================================
不同类型的指针的区别以及与数组的关系
极端例子如下:
linux下示例代码如下:
#include <stdio.h> int main()
{
int a = 0x12345678;
char *p = (char *)&a;
printf("%x, %x, %x, %x, %x\n", *p, p[], p[], p[], p[]); //%x的意思是按照十六进制的有符号整数输出(小写)
printf("--------------------\n"); *p = ;
p[] = ;
printf("%08x\n", a);
printf("--------------------\n"); char b[] = { };
int *p1 = (int *)&b;
p1[] = 0x12345678; int i;
for (i = ; i < ; i++)
{
printf("b[%d] = %x\n", i, b[i]);
}
printf("--------------------\n"); return ;
} 输出的结果是:
, , , ,
-------------------- --------------------
b[] =
b[] =
b[] =
b[] =
b[] =
b[] =
b[] =
b[] =
b[] =
b[] =
b[] =
b[] =
b[] =
b[] =
b[] =
b[] =
b[] =
b[] =
b[] =
b[] =
--------------------
说明:小端对齐输出。
小结:c语言中所有的数据类型都可以理解为一个char的数组。
-----------------------------------------------------------------------------
小案例:int类型与ip地址的对应关系。
实际上的ip地址是一个无符号的整数构成的。1个int,4个字节。
1、把整数转换为ip地址
ip地址的格式:
0.0.0.0 ~ 255.255.255.255
linux下示例代码如下:
#include <stdio.h> int main()
{
unsigned int a = ; //0x 0e 08 47 23 (14.8.71.35) 0e的0可以省略哦!
scanf("%u", &a);
unsigned char *p = (unsigned char*)&a;
printf("%u.%u.%u.%u\n", p[], p[], p[], p[]);
printf("---------------------\n"); int i;
for (i = ; i < ; i++)
{
printf("%2x,%2u\n", p[i], p[i]);
} return ;
}
输出结果是: 108.8.25.251
---------------------
fb,
,
,
6c,
-----------------------------------------------------------------------------
2、把ip地址转换为整数
输入一个ip地址
char a[100] = "192.168.2.5"
把这个ip转化为unsigned int类型的整数。
linux下示例代码如下:
#include <stdio.h> int main()
{
char a[] = "192.168.2.5";
unsigned int ip = ;
unsigned char *p = (unsigned char *)&ip;
int a1, a2, a3, a4; sscanf(a, "%d.%d.%d.%d\n", &a1, &a2, &a3, &a4); //sscanf从某一个格式化字符串中读取到我们想要的东西,找到后通过转义的方式取出来.
//printf("%d.%d.%d.%d\n", a1, a2, a3, a4); p[] = a4;//*p = a4; 二者等价
p[] = a3;
p[] = a2;
p[] = a1; /*
*p = a4;
p++;
*p = a3;
p++;
*p = a2;
p++;
*p = a1;
*/ printf("%u\n", ip); return ; }
输出结果是: root@iZ2zeeailqvwws5dcuivdbZ:~///指针# gcc -o p16 p16.c
root@iZ2zeeailqvwws5dcuivdbZ:~///指针# p16 root@iZ2zeeailqvwws5dcuivdbZ:~///指针# ping
PING (192.168.2.5) () bytes of data.
-----------------------------------------------------------------------------
3、使用指针给二维数组排序
linux下示例代码如下:
#include <stdio.h> int main()
{
char a[][] = { { , , , , }, { , , , , } };
char *p = (char *)a; int i, j;
for (i = ; i < ; i++)
{
for (j =; j < - i; j++)
{
if (p[j] < p[j - ])
{
char tmp = p[j];
p[j] = p[j - ];
p[j - ] = tmp; }
}
} for (i = ; i < ; i++)
{
for (j = ; j < ; j++)
{
printf("%d\n", a[i][j]);
}
} return ; } 输出的结果是:
=============================================================================
指针数组
linux下示例代码如下:
#include <stdio.h> int main()
{
char *a[]; //定义了一个指针数组,指针数组的名字叫a,每个成员是char *类型,一共10个成员。
int *b[]; //定义了一个指针数组,指针数组的名字叫b,每个成员是char *类型,一共10个成员。 printf("%lu, %lu\n", sizeof(a), sizeof(b)); //80, 80 int i = ;
//a = &i; //指针数组名不能做左值。
//b = &i; //指针数组名不能做左值。 b[] = &i; //b[0]的类型是int *。
printf("%lu, %lu\n", sizeof(b[]), sizeof(*b[])); //8, 4 *b[0]是一个数组的成员。 return ; }
-----------------------------------------------------------------------------
linux下示例代码如下:
#include <stdio.h> int main()
{
int *a[] = { NULL }; //定义了一个指针数组,指针数组的名字叫a,每个成员是int *类型的,一共有10个成员。
int b, c, d; //对于指针数组来说,要先有指针的性质,再有数组的性质,即先得获得地址,然后对数组进行操作。 a[] = &b; //a是指针数组的名字。
a[] = &c; //a[0]是指针变量,
a[] = &d; *a[] = ; //*a[0]是一个数组的成员之一。 printf("%d\n", b); return ;
}
=============================================================================
二级指针(指向指针的指针)
指针是一个变量,既然是变量就也存在内存地址,所以可以定义一个指向指针的指针。
linux下示例代码如下:
#include <stdio.h> int main()
{
int a = ;
int *p = &a;
int **pp = &p; //二级指针:指向指针的指针。
pp = &p; //pp代表一级指针内存地址的编号。 //*pp = 1000; //等号左端是指针类型的变量,等号右边是int类型。
//或者可以这么理解,指针变量指的是一个地址,无法对它进行赋值。
//再或者说,我们两对应的内存地方不一样。操作系统不让你干这非法的事情。(分段错误) **pp = ;
printf("%d\n",a ); // return ;
}
二级指针说明图如下:
=============================================================================
三级指针及其以上指针
linux下示例代码如下:
#include <stdio.h> int main()
{
int a = ;//零级“指针”,a有内存地址编号,a的内容(值)为0。
int *p = &a;//一级指针,p代表a的内存地址编号;*p代表a的内容(值)。
int **pp = &p;//二级指针,pp代表p的内存地址编号;*pp代表a的内存地址编号;**pp代表a的内容(值)。
int ***ppp = &pp;//三级指针,ppp代表pp的内存地址编号;*ppp代表p的内存地址编号;**ppp代表a的内存地址编号,***ppp代表a的内容(值)。
int ****pppp = &ppp;//四级指针 ****pppp = ;
printf("a = %d\n", a); // return ;
}
pppp代表ppp的内存地址编号;
*pppp代表pp的内存地址编号;
**pppp代表p的内存地址编号;
***pppp代表a的内存地址编号;
****pppp代表a的内容(值)。
linux下示例代码如下图所示:
特别注意:
能用一级指针解决的问题不要用二级指针,能用二级指针解决的不用三级指针,指针级数过多会导致程序很复杂。
工作中大量使用的是一级指针,二级指针也很常用,三级指针就很罕见了,四级指针几乎没有。但笔试会考你哦!可以画图解决!
=============================================================================
函数的参数为指针变量(指针变量作为函数的参数)
实际上指针更多的时候用在函数的参数上。
函数的参数可以使是指针类型。它的作用是将一个变量的地址编号传送给另一个函数。
void test(int *p); //定义一个函数,形参是int *类型。
c语言中如果想通过在一个函数的内部修改外部实参的值,那么就需要给函数的参数传递这个实参的地址。
-----------------------------------------------------------------------------
linux下示例代码如下:
#include <stdio.h> void swap(int a, int b) //swap是交换的意思。
{
int tmp = a;
a = b;
b = tmp;
printf("a = %d, b = %d\n", a, b);//a = 2, b = 1 形参中的值发生变化了。
} int main()
{
int a = ;
int b = ; swap(a, b);
printf("a = %d, b = %d\n", a, b);//a = 1, b = 2 c语言中实参的值会传给形参,而形参值的改变并不会影响到实参。
return ;
}
-----------------------------------------------------------------------------
那么现在我就想在一个函数的内部修改外部实参的值,那么就需要给函数的参数传递这个实参的地址。代码如下: #include <stdio.h> void swap(int *a, int *b)
{
int tmp = *a;
*a = *b;
*b = tmp;
printf("a = %d, b = %d\n", a, b);//a = 2, b = 1 形参中的值发生变化了。
} int main()
{
int a = ;
int b = ; swap(&a, &b); //那么现在我就想在一个函数的内部修改外部实参的值,那么就需要给函数的参数传递这个实参的地址。
printf("a = %d, b = %d\n", a, b);//a = 2, b = 1 实参中的值发生变化了。
return ;
}
函数参数是指针变量的画图说明如下:
即:c语言想通过函数内部来修改实参的值,只能给函数传递实参的地址来间接的修改实参的值。
例如scanf函数:
int a;
scanf("%d", &a);//scanf是一个函数,现在要通过函数内部来修改实参a的值,只能用传递a的地址的方式修改a的值
=============================================================================
函数的参数为数组名时(即数组名作为函数的参数)
当一个数组名作为函数的形参的时候,c语言将数组名解释为指针变量,其实是一个指针变量名。
如果数组名作为函数的参数,那么这个就不是数组名了,而是一个指针变量名。
当把一个数组名作为函数的参数时,修改形参的值的时候,同时也影响实参的数组成员的值。
如果把一个数组名作为函数的参数,那么在函数内部就不知道这个数组的元素个数了,需要再增加一个参数来标明这个数组的大小。
如果将一个数组作为函数的形参进行传递,那么数组的内容可以在被调用函数的内部进行修改,
有时候不希望这样的事情发生,所以要对形参采用const进行修饰。
-----------------------------------------------------------------------------
linux下示例代码如下:
#include <stdio.h> //此三种写法均可:常写的是void test(int *a)这一种!
//void test(int a[10])
//void test(int a[])
/*
void test(int *a)
{
printf("%lu\n", sizeof(a));
a[50] = 50;
}
*/ /*
//为了从语法的角度不让在函数的内部修改数组成员的值,用const进行限定,如下:
void test(const int *a)
{
printf("%lu\n", sizeof(a));
//a[5] = 100; 该句编译通不过,出现错误。因为此时的数组只能读,不能改。
}
*/ //现在我非要在加有const的函数内部进行修改呢?可以,使用指针就可以绕过c语言的语法!
void test(const int *a)
{
printf("%lu\n", sizeof(a)); int *p = (int *)a;
p[] = ; //此时可以修改了!
} int main()
{
int a[] = { , , , , , , , , , };
printf("%lu\n", sizeof(a));
printf("----------------------\n"); test(a); int i;
for (i = ; i < ; i++)
{
printf("a[%d] = %d\n", i, a[i]);
} return ;
}
=============================================================================
函数的返回值为指针时(即指针作为函数的返回值)
int *test() //函数的返回值类型是指针类型(具体的讲解在下一节:内存管理)
{
return NULL;
}
=============================================================================
几个c语言的库函数:memset、memcpy、memmove函数,使用的时候需要包含头文件 #include <string.h>
这三个函数分别实现内存设置、内存复制、内存移动功能。
--------------------------------------
memset的功能是:将指定区域的内存置空(设置为0)。
void *memset(void *s, int c, size_t n);
第一个参数是:指定要置空的内存的首地址;
第二个参数是:要设置的值,一般写0;
第三个参数是:这块内存的大小,单位:字节。
linux下示例代码如下:
#include <stdio.h>
#include <string.h> int main()
{
int a[] = { , , , , , , , , , };
//想把一个已经初始化的数组成员的值都变成0。 //法一:传统的方法如下:
//a[10] = { 0 };//不能这样来啊,这叫定义后立马进行初始化。
/*
int i;
for (i = 0; i < 10; i++)
{
a[i] = 0;
}
*/ //法二:使用c语言库函数memset。
memset(a, , sizeof(a)); int i;
for (i = ; i < ; i++)
{
printf("a[%d] = %d\n", i, a[i]);
} return ;
}
-----------------------------------------------------------------------------
memcpy功能是:两块内存之间拷贝数据。
使用memcpy时,首先一定要确保内存没有重叠区域。
void *memcpy(void *dest, const void *src, size_t n);
第一个参数是:目标地址(目标内存首地址);
第二个参数是:源地址(源内存首地址);
第三个参数是:拷贝多少内容,单位字节。
linux下示例代码如下:
#include <stdio.h>
#include <string.h> int main()
{
//把a的内容拷贝到b中去。
int a[] = { , , , , , , , ,, };
int b[] = { }; memcpy(b, a, sizeof(a)); int i;
for (i = ; i < ; i++)
{
printf("b[%d] = %d\n", i, b[i]);
} return ;
}
输出的结果是:
b[] =
b[] =
b[] =
b[] =
b[] =
b[] =
b[] =
b[] =
b[] =
b[] =
--------------------------------------
linux下示例代码如下:
#include <stdio.h>
#include <string.h> int main()
{
//把a的内容拷贝到b中去。将int改为short。
short a[] = { , , , , , , , ,, };
int b[] = { }; memcpy(b, a, sizeof(a)); int i;
for (i = ; i < ; i++)
{
printf("b[%d] = %d, %08x\n", i, b[i], b[i]);
} return ;
}
输出的结果是:
b[] = ,
b[] = ,
b[] = ,
b[] = ,
b[] = , 000a0009
b[] = ,
b[] = ,
b[] = ,
b[] = ,
b[] = ,
内存拷贝说明画图如下:
-----------------------------------------------------------------------------
memmove功能是:内存移动,参数与memcpy一致。
void *memmove(void *dest, const void *src, size_t n);
第一个参数是:目标地址(目标内存首地址);
第二个参数是:源地址(源内存首地址);
第三个参数是:拷贝多少内容,单位字节。
内存重叠区域说明如下图所示:
=============================================================================
指针小结
定义 说明
int i; 定义个一个int类型的变量
int *p; 定义一个指向int类型的指针变量
int a[10]; 定义一个int类型的数组
int *p[10]; 定义一个指针数组,其中每一个数组元素指向一个int类型变量的地址
int func(); 定义一个函数,返回值类型为int类型
int *func(); 定义一个函数,返回值类型为int*类型
int **p; 定义一个指向int类型的指针的指针,二级指针
=============================================================================
字符指针 与 字符串
在c语言中,大多数的字符串操作其实就是指针操作。
--------------------------------------
1、通过指针访问字符串数组
linux下示例代码如下:
#include <stdio.h> int main()
{
char a[] = "hello world";
char *p = a; *p = 'a'; //注意:*p = p[0]
p[] = 'b';
printf("%s\n", a); //aelbo world return ;
}
-----------------------------------------------------------------------------
通过指针使得字符串逆置
法一:使用一个指针
linux下示例代码如下:
#include <stdio.h>
#include <string.h> int main()
{
char a[] = "hello world";
char *p = a; int len = strlen(a);
int min = ;
int max = len - ; while (min < max)
{
//char tmp = *p;//等价于tmp = p[0];
char tmp = *(p + min);
*(p + min) = *(p + max);
*(p + max) = tmp;
max--;
min++;
} printf("%s\n", a); //dlrow olleh return ;
}
--------------------------------------
法二:使用二个指针
linux下示例代码如下:
#include <stdio.h>
#include <string.h> int main()
{
char a[] = "hello world";
char *p = a; //等价于 char *p; p = a;//p指向了数组的首元素的地址。
char *p1 = a; //等价于 char *p1 = p; int len = strlen(a); p1 += len - ;//p1指向了数组的最后一个元素的地址。 while (p < p1)
{
char tmp = *p;
*p = *p1;
*p1 = tmp;
p++;
p1--;
} printf("%s\n", a); //dlrow olleh return ;
}
=============================================================================
函数的参数为char *(即char *作为函数的参数)
linux下示例代码如下:
#include <stdio.h> //三者形式等价,钟爱第三种!
//void test(char a[10])
//void test(char a[])
void test(char *a) //数组作为函数的形参时,相当于一级指针。
{
printf("%s\n", a);
a[] = '';
} int main()
{
char a[] = "abcd";
test(a);
printf("%s\n", a); return ;
}
输出结果是:
abcd
abc4
如果将一个数组作为函数的形参进行传递,那么数组的内容可以在被调用函数的内部进行修改,
有时候不希望这样的事情发生,所以要对形参采用const进行修饰。代码如下:
void test(const char *a)
{
printf("%s\n", a);
//a[3] = '4';
}
-----------------------------------------------------------------------------
自定义函数实现求字符串长度和字符串拷贝
linux下示例代码如下:
#include <stdio.h> int mystrlen(const char *s)
{
int len = ;
while (s[len])
{
len++;
}
return len;
} void mystrcpy(char *s1, const char *s2)
{
int len = ;
while (s2[len])
{
s1[len] = s2[len];
len++;
}
} int main()
{
char a[] = "abc";
char b[] = { }; int i = ;
i = mystrlen(a);
mystrcpy(b, a); printf("%d\n", i);
printf("%s\n", b);
return ;
}
输出的结果是: abc
-----------------------------------------------------------------------------
如果一个数组作为函数的参数,那么数组的成员数量在函数内部是不可见的。
解决方法:在传递一个数组的时候,需要同时提供另外一个参数,标明这个数组有几个成员变量。
例外:如果函数的参数是一个字符串时,那么并不需要再传递一个参数说明这个字符串有多长。
linux下示例代码如下:
#include <stdio.h> int printf_array(int n, int *a)
{
int i;
for (i = ; i < n; i++)
{
printf("%d\n", a[i]);
}
} int main()
{
int a[] = { , , , , , , , , , };
printf_array(sizeof(a) / sizeof(a[]), a); return ;
} 注意:size_t 单位是1个字节。
=============================================================================
指针数组作为main函数的形参
先来看一个指针数组作为函数的参数(此时把指针数组解释为二级指针)
linux下示例代码如下:
#include <stdio.h> //int print(char *p[10])
//int print(char *p[])
int print(int n, char **p) //指针数组作为函数的形参时,相当于二级指针。
{
int i;
for (i = ; i < n; i++)
{
printf("%s\n", p[i]); //p[0]、p[1]、p[1]等的类型是char *。
printf("%c\n", *p[i]); //*p[0]、*p[1]等的类型是char。
}
} int main()
{
char *a[]; //a[0]、a[1]、a[2]、a[3]分别指向一个char *类型的数组。 char a1[] = "hello";
char a2[] = "abc";
char a3[] = "world";
char a4[] = "haha"; a[] = a1;
a[] = a2;
a[] = a3;
a[] = a4; printf("%lu, %lu\n", sizeof(a), sizeof(a[])); print(sizeof(a) / sizeof(a[]), a); return ; }
输出结果是:
,
hello
h
abc
a
world
w
haha
h
-----------------------------------------------------------------------------
linux下示例代码如下:
#include <stdio.h> int main(int argc, char **args)//argc代表这个数组有多少成员,args是一个指针数组,args这个数组的每个成员类型是char *,
{
int i;
for (i = ; i < argc; i++)
{
printf("%s\n", args[i]);
} return ;
}
args是命令行参数的字符串数组,argc代表命令行参数的数量,程序名字本身就算一个参数。
main函数是由系统调用的,所以main函数的参数功能是:得到命令行的参数。
-----------------------------------------------------------------------------
举个小例子:用到main函数的参数,实现计算两个数的和
例如:
程序名 数1 数2
一回车结果就出来了。
a 15 45
60
linux下示例代码如下:
#include <stdio.h>
#include <stdlib.h> int main(int argc, char **args) //注意:main函数的参数的类型、和顺序是不能修改的。
{
if (argc <= )
{
printf("参数不足,使用方法:%s 整数1 整数2\n", args[]);
return ;
}
else
{
int a = atoi(args[]);
int b = atoi(args[]);
printf("%d\n", a + b);
} return ;
}
-----------------------------------------------------------------------------
课后作业
写一个程序,需要用到main函数的参数
例如:
程序名 整数1 运算符 整数2,程序运行的结果是计算结果。
a 5 + 6 注意:中间的加号是字符串。
11
a 5 * 6
30
......
+ - * / 都要实现
linux下示例代码如下:
#include <stdio.h>
#include <stdlib.h> int main(int argc, char **args) //等价于 char *args[]
{
//因为星号在linux下是一个通配符,代表在当前目录下的所有文件。 /*
//验证星号是什么?
int i;
for (i = 0; i < argc; i++)
{
printf("%s\n", argc[i]);
}
return 0;
*/ if (argc < )
{
return ;
} //因为 char **args,是指针数组,args是二级指针,分别是 args[0]、args[1]等,类型是char *。
int a = atoi(args[]); //把第一个参数转化为int。
int b = atoi(args[]); //把第三个参数转化为int。 //“+” 中间的加号是一个字符串。该字符串是一个字符数组,且对于该字符串的加号是第一个元素。
char *s = args[]; //得到第二个参数,因为每个参数的类型都是char *。
char c = s[];
//char c = args[2][0]; //该句话与上面两句话等价。 switch (c)
{
case '+':
printf("%d\n", a + b);
break;
case '-':
printf("%d\n", a - b);
break;
case '*':
printf("%d\n", a * b);
break;
case '/':
printf("%d\n", a / b);
break;
default:
printf("error\n");
} return ;
}
执行结果为:
"p35.c" 51L, 1226C written
root@iZ2zeeailqvwws5dcuivdbZ:~///指针# gcc -o a p35.c
root@iZ2zeeailqvwws5dcuivdbZ:~///指针# a + root@iZ2zeeailqvwws5dcuivdbZ:~///指针# a - root@iZ2zeeailqvwws5dcuivdbZ:~///指针# a \* (注意要有转义字符) root@iZ2zeeailqvwws5dcuivdbZ:~///指针# a / root@iZ2zeeailqvwws5dcuivdbZ:~///指针# a
root@iZ2zeeailqvwws5dcuivdbZ:~///指针#
=============================================================================
c语言基础学习07的更多相关文章
- Golang 汇编asm语言基础学习
Golang 汇编asm语言基础学习 一.CPU 基础知识 cpu 内部结构 cpu 内部主要是由寄存器.控制器.运算器和时钟四个部分组成. 寄存器:用来暂时存放指令.数据等对象.它是一个更快的内存. ...
- D02-R语言基础学习
R语言基础学习——D02 20190423内容纲要: 1.前言 2.向量操作 (1)常规操作 (2)不定长向量计算 (3)序列 (4)向量的删除与保留 3.列表详解 (1)列表的索引 (2)列表得元素 ...
- D01-R语言基础学习
R语言基础学习——D01 20190410内容纲要: 1.R的下载与安装 2.R包的安装与使用方法 (1)查看已安装的包 (2)查看是否安装过包 (3)安装包 (4)更新包 3.结果的重用 4.R处理 ...
- D03——C语言基础学习PYTHON
C语言基础学习PYTHON——基础学习D03 20180804内容纲要: 1 函数的基本概念 2 函数的参数 3 函数的全局变量与局部变量 4 函数的返回值 5 递归函数 6 高阶函数 7 匿名函数 ...
- D03-R语言基础学习
R语言基础学习——D03 20190423内容纲要: 1.导入数据 (1)从键盘输入 (2)从文本文件导入 (3)从excel文件导入 2.用户自定义函数 3.R访问MySQL数据库 (1)安装R ...
- go语言基础学习
go基础学习,面向对象-方法在Go语言中,可以给任意自定义类型(包括内置类型,但不包括指针类型)添加相应的方法 使用= 和:=的区别: // = 使用必须使用先var声明例如: var a a=100 ...
- C# 语言基础学习路线图
一直以来,对于很多知识点都是存于收藏夹中,随着时间地变更,收藏夹中链接也起来越多,从未进行整理,也很零散,所以想对曾经遇到并使用过的一些知识形成文档,作为个人知识库的一部分. 就从C# 语言基础开始, ...
- C语言基础学习基本数据类型-变量的命名
变量的命名 变量命名规则是为了增强代码的可读性和容易维护性.以下为C语言必须遵守的变量命名规则: 1. 变量名只能是字母(A-Z,a-z),数字(0-9)或者下划线(_)组成. 2. 变量名第一个字母 ...
- 从零开始系列-R语言基础学习笔记之二 数据结构(二)
在上一篇中我们一起学习了R语言的数据结构第一部分:向量.数组和矩阵,这次我们开始学习R语言的数据结构第二部分:数据框.因子和列表. 一.数据框 类似于二维数组,但不同的列可以有不同的数据类型(每一列内 ...
随机推荐
- shell按行读取文件
这工作小半年了发现以前学的那么多流弊技能都不怎么用,倒是shell用的很多,自己已经从shell小菜鸟一步步走过来,已经要变成大菜鸟=.= 经常需要用shell按行读取配置文件,自己在上面踩了很多坑, ...
- df 命令详解
一.df 作用: 显示磁盘分区上的可使用的磁盘空间, 默认显示单位为kb . 可以利用该命令来获取硬盘被占用了多少空间,目前还剩下多少空间的等信息. 选项: -a :包含全部的文件系统 -h :以 ...
- 学习时用的软件最新 开发环境为Visual Studio 2010,数据库为SQLServer2005,使用.net 4.0开发。 超市管理系统
一.源码特点 1.采用典型的三层架构进行开发.模板分离,支持生成静态 伪静态..购物车.登陆验证.div+css.js等技术二.功能介绍 1.本源码是一个超市在线购物商城源码,该网上商城是给超市便利店 ...
- Linux(CentOS6.5_X86.64)编译libjpeg出现“checking host system type... Invalid configuration `x86_64-unknown-linux-gnu': machine `x86_64-unknown' not recognized”的解决
本文地址http://comexchan.cnblogs.com/,作者Comex Chan,尊重知识产权,转载请注明出处,谢谢! 今天在编译libjpeg 的时候,遇到下面的报错: checki ...
- 云计算---openstack镜像制作详解
一:本地部署KVM 1.安装KVM 1.1安装须知 查看CPU是否支持kvm完全虚拟机. [root@LINUX ~]# grep "flags" /proc/cpuinfofla ...
- 2、公司部门的组成 - CEO之公司管理经验谈
今天讲讲公司部门的组成.公司部门一般是根据公司业务来进行划分的,IT企业和其它企业的部门划分有一定的区别.企业部门的划分还是比较重要的,部门主要明确各部门所具有的自己的职责.这里对IT企业的部门做了一 ...
- js 对象的值传递
一.变量赋值的不同 1.原始值 在将一个保存着原始值的变量复制给另一个变量时,会将原始值的副本赋值给新变量,此后这两个变量是完全独立的. 2.引用值: 在将一个保存着对象内存地址的变量复制给另一个变量 ...
- ssh的免密登陆
想必大家都有使用ssh登陆的过程了,那么,怎么设置ssh免密登陆呢?下面有一些我的总结: 环境:服务器主.从 主服务器:192.168.1.1 从服务器:192.168.1.2 实现主服务器ssh登录 ...
- ThreadPool.QueueUserWorkItem引发的血案,线程池异步非正确姿势导致程序闪退的问题
ThreadPool是.net System.Threading命名空间下的线程池对象.使用QueueUserWorkItem实现对异步委托的先进先出有序的回调.如果在回调的方法里面发生异常则应用程序 ...
- sql server中的分页数据查询
1.引言 今天在工作中遇到一个需要进行sql server分页数据查询的问题,但是分页数据查询的sql却忘记了,最终通过查询资料解决了该问题.现在把解决方法记下,以备查阅. 在这里需要感谢博客园的Ql ...