目录
一、指针的本质分析
1、*号的意义
(1)指针的声明和使用
(2)实践:指针使用示例
2、传值调用与传址调用
(1)什么是传值调用,传址调用?
(2)实践:利用指针交换变量
3、常量与指针
(1)const与指针
(2)实践:const与指针
4、小结
二、数组的本质分析
1、数组的概念
2、数组的大小
(1)数组大小的获取(内存、元素数量)
(2)实践:数组的初始化
3、数组地址与数组名
(1)数组地址与数组名
(2)实践:数组名和数组地址
4、数组名的盲点
(1)数组名的易错点
(2)实践:数组和指针并不相同
5、小结
三、指针和数组分析(上)
1、数组的本质
(1)什么是数组?
(2)实践:a + 1的结果是什么?
2、指针的运算
3、指针的比较
(1)指针的比较
(2)实践:指针运算初探
(3)实践:指针运算的应用
4、小结
四、指针和数组分析(下)
1、数组的访问方式
2、下标形式VS 指针形式
(1)下标形式与指针形式的效率及转化问题
(2)实践:数组的访问方式
(3)实践:数组和指针不同
3、a和&a的区别
(1)首元素地址和数组地址的区别
(2)实践:指针运算经典问题
4、数组参数
(1)数组作为参数入参
(2)实践:虚幻的数组参数
5、小结
五、C语言中的字符串
1、字符串的概念
2、字符数组与字符串
(1)字符串的本质
(2)实践:字符数组与字符串
3、鲜为人知的小秘密
4、字符串字面量
(1)“Hello World ! ”是一个无名的字符数组
(2)实践:字符串字面量的本质
5、字符串的长度
(1)字符串的长度
(2)实践:strlen的使用
6、小结
六、字符串典型问题分析
1、snprintf 函数
2、结束符'\0'
3、strcmp函数
4、字符串循环右移
七、数组指针和指针数组分析
1、数组类型
2、定义数组类型
3、数组指针
(1)数组指针的定义
(2)实践:数组类型和数组指针
4、指针数组
(1)指针数组的定义
(2)实践:指针数组的应用
5、小结
八、main函数与命令行参数
1、main函数的概念
(1)main函数的概念
2、main函数的本质
(1)main函数的本质
3、main函数的参数
(1)main函数的参数
(2)实践:main函数的参数
4、小技巧
(1)面试中的小问题
(2)实践:gcc中的属性关键字
5、小结
九、多维数组和多维指针
1、指向指针的指针
2、为什么需要指向指针的指针?
实践:重置动态空间大小
3、二维数组与二级指针
实践:遍历二维数组
4、数组名
(1)一维数组和二维数组及其数组名
(2)实践:如何动态申请二维数组
5、小结
十、数组参数和指针参数分析
1、退化的意义
2、二维数组参数
3、等价关系
4、被忽视的知识点
实践:传递与访问二维数组
5、小结
十一、函数与指针分析
1、函数类型
2、函数指针
实践:函数指针的使用
3、回调函数
实践:回调函数使用示例
4、小结
十二、指针阅读技巧分析
1、笔试中的问题
2、指针阅读技巧解析
3、小结
一、指针的本质分析
1、*号的意义
(1)指针的声明和使用
- 在指针声明时,*号表示所声明的变量为指针
- 在指针使用时,*号表示取指针所指向的内存空间中的值
- *号类似一把钥匙,通过这把钥匙可以打开内存,读取内存中的值。
int i = 0;
int j = 0;
//指针声明
int* p = &i;
//取值
j = *p;
//赋值
*p = 10;
图解如下:
(2)实践:指针使用示例
- 指针变量所占内存都是4字节(32位系统)
- 指针类型决定访问内存时的长度范围
内存是由字节组成的,每个字节都有一个编号。指针变量主要是存放相同数据类型的变量的首地址。这里的这个地址其实就是内存的某个字节的编号。而这个编号的确定是与地址总线有关。如果地址总线是32位,则它的寻址范围是0~2^32(0~4G)。那么为一个字节的编址就会由32个0或者1组成。例如第一个字节的编址是32个0,最后一个的编址是32个1。一个字节有8位,32位则需要4个字节。
简单的说32位的操作系统就是指:地址总线是32位的系统。那么,也就是说操作系统的位数决定了指针变量所占的字节数。
#include <stdio.h>
int main()
{
int i = 0;
int* pI;
char* pC;
float* pF;
pI = &i; // 将i的地址赋值给pI
*pI = 10; // 间接将10赋值给i
printf("%p, %p, %d\n", pI, &i, i); // i的地址 i的地址 i的值
printf("%d, %d, %p\n", sizeof(int*), sizeof(pI), &pI); // 4 4 pI的地址
printf("%d, %d, %p\n", sizeof(char*), sizeof(pC), &pC); // 4 4 pC的地址
printf("%d, %d, %p\n", sizeof(float*), sizeof(pF), &pF);// 4 4 pF的地址
return 0;
}
2、传值调用与传址调用
(1)什么是传值调用,传址调用?
- 指针是变量,因此可以声明指针参数;
- 当一个函数体内部需要改变实参的值,则需要使用指针参数;
- 函数调用时实参值将复制到形参;
- 指针适用于复杂数据类型作为参数的函数中;
- 传值调用并不改变传入的变量;
- 传址调用是传入变量地址,从而可以改变传入的地址对应的值。
(2)实践:利用指针交换变量
① 传值调用:这部分代码并不能够实现交换变量的值,这就是传值调用。
// 这部分代码并不能够实现交换变量的值,这就是传值调用
#include <stdio.h>
int swap(int a, int b)
{
int c = a;
a = b;
b = c;
}
int main()
{
int aa = 1;
int bb = 2;
printf("aa = %d, bb = %d\n", aa, bb);
swap(aa, bb);
printf("aa = %d, bb = %d\n", aa, bb);
return 0;
}
② 传址调用:利用指针交换变量
#include <stdio.h>
int swap(int* a, int* b) // 传入指针
{
int c = *a;
*a = *b; // 利用指针改变变两个值
*b = c; // 利用指针改变变两个值
}
int main()
{
int aa = 1;
int bb = 2;
printf("aa = %d, bb = %d\n", aa, bb);
swap(&aa, &bb);
printf("aa = %d, bb = %d\n", aa, bb);
return 0;
}
3、常量与指针
(1)const与指针
const int* p; // p可变,p指向的内容不可变
int const* p; // p可变,p指向的内容不可变
int*const p; // p不可变,p指向的内容可变
const int* const p; // p和p指向的内容都不可变
方法:不看数据类型,如果是*p代表指向的地址的数值不能变,如果是p代表地址不能变。
口诀∶左数右指
- 当const出现在*号左边时指针指向的数据为常量
- 当const出现在*后右边时指针本身为常量
(2)实践:const与指针
#include <stdio.h>
int main()
{
int i = 0;
const int* p1 = &i;
int const* p2 = &i;
int* const p3 = &i;
const int* const p4 = &i;
*p1 = 1; // compile error
p1 = NULL; // ok
*p2 = 2; // compile error
p2 = NULL; // ok
*p3 = 3; // ok
p3 = NULL; // compile error
*p4 = 4; // compile error
p4 = NULL; // compile error
return 0;
}
4、小结
- 指针是C语言中一种特别的变量
- 指针所保存的值是内存的地址
- 可以通过指针修改内存中的任意地址内容
二、数组的本质分析
1、数组的概念
2、数组的大小
(1)数组大小的获取(内存、元素数量)
- 数组在一片连续的内存空间中存储元素
- 数组元素的个数可以显示或隐式指定
(2)实践:数组的初始化
#include <stdio.h>
int main()
{
int a[5] = { 1, 2 };
int b[] = { 1, 2 };
printf("a[2] = %d\n", a[2]); // 默认为零
printf("a[3] = %d\n", a[3]);
printf("a[4] = %d\n", a[4]);
printf("sizeof(a) = %d\n", sizeof(a)); // 整个数组的所占内存大小 5 * 4 = 20byte
printf("sizeof(b) = %d\n", sizeof(b)); // 整个数组的所占内存大小 2 * 4 = 8byte
printf("count for a: %d\n", sizeof(a) / sizeof(int));// 数组所包含的元素数量 20 / 4 = 5
printf("count for b: %d\n", sizeof(b) / sizeof(*b)); // 数组所包含的元素数量 8 / 4 = 2
return 0;
}
3、数组地址与数组名
(1)数组地址与数组名
- 数组名代表数组首元素的地址
- 数组的地址需要用取地址符&才能得到
- 数组首元素的地址值与数组的地址值相同
- 数组首元素的地址与数组的地址是两个不同的概念
(2)实践:数组名和数组地址
#include <stdio.h>
int main()
{
int a[5] = { 0 }; // 将所有元素赋值为 0
printf("a = %p\n", a); // 数组名 -- 数组首元素的地址
printf("&a = %p\n", &a); // 取数组的地址
printf("&a[0] = %p\n", &a[0]); // 数组首元素的地址
return 0;
}
4、数组名的盲点
(1)数组名的易错点
- 数组名可以看做一个指针常量(无法改变 )
- 数组名“指向”的是内存中数组首元素的起始位置
- 数组名不包含数组的长度信息
- 在表达式中数组名只能作为右值使用(因为无法改变 )
- 只有在下列场合中数组名不能看做指针常量
- 数组名作为sizeof操作符的参数 取的是数组所占得内存
- 数组名作为&运算符的参数 取的是数组的地址
(2)实践:数组和指针并不相同
#include <stdio.h>
int main()
{
int a[5] = { 0 };
int b[2];
int* p = NULL;
p = a; // 将数组的首元素地址 赋值给 p
printf("a = %p\n", a); // 打印数组的首元素地址
printf("p = %p\n", p); // 打印数组的首元素地址
printf("&p = %p\n", &p);// 打印指针的地址
printf("sizeof(a) = %d\n", sizeof(a)); //打印数组所占内存 20byte
printf("sizeof(p) = %d\n", sizeof(*p));//打印数组首元素所占内存 4byte
printf("sizeof(p) = %d\n", sizeof(*(p + 1)));//打印数第二个元素所占内存 4byte
printf("sizeof(p) = %d\n", sizeof(p)); //打印指针所占内存 4byte
printf("\n");
p = b; // 将数组的首元素地址 赋值给 p
printf("b = %p\n", b); // 打印数组的首元素地址
printf("p = %p\n", p); // 打印数组的首元素地址
printf("&p = %p\n", &p); // 打印指针的地址
printf("sizeof(b) = %d\n", sizeof(b)); // 打印数组所占内存 8byte
printf("sizeof(p) = %d\n", sizeof(p)); // 打印指针所占内存 4byte
// b = a; 数组间不可直接赋值
return 0;
}
5、小结
- 数组是一片连续的内存空间
- 数组的地址和数组首元素的地址意义不同
- 数组名在大多数情况下被当成指针常量处理
- 数组名其实并不是指针,不能将其等同于指针
三、指针和数组分析(上)
1、数组的本质
(1)什么是数组?
- 数组是一段连续的内存空间
- 数组的空间大小为sizeof(array_type)* array_size 数组元素的类型大小 x 元素的数量
- 数组名可看做指向数组第一个元素的常量指针
(2)实践:a + 1的结果是什么?
#include <stdio.h>
int main()
{
int a[5] = {0};
int* p = NULL;
printf("a = 0x%X\n", (unsigned int)(a)); // 数组名地址
printf("a + 1 = 0x%X\n", (unsigned int)(a + 1)); // 数组名地址 + n * 数据类型大小
printf("p = 0x%X\n", (unsigned int)(p)); // 数组名地址
printf("p + 1 = 0x%X\n", (unsigned int)(p + 1)); // 数组名地址 + n * 数据类型大小
return 0;
}
2、指针的运算
- 指针是一种特殊的变量,与整数的运算规则为
结论∶
当指针p指向一个同类型的数组的元素时:p+1将指向当前元素的下一个元素;p-1将指向当前元素的上一个元素。
- 指针之间只支持减法运算
- 参与减法运算的指针类型必须相同
注意∶
1、 只有当两个指针指向同一个数组中的元素时,指针相减才有意义,其意义为指针所指元素的下标差
2、 当两个指针指向的元素不在同一个数组中时,结果未定义
3、指针的比较
(1)指针的比较
- 指针也可以进行关系运算(<,<=,>,>=)
- 指针关系运算的前提是同时指向同一个数组中的元素
- 任意两个指针之间的比较运算( ==,!= )无限制
- 参与比较运算的指针类型必须相同
(2)实践:指针运算初探
- 指针关系运算的前提是同时指向同一个数组中的元素
- 指针运算没有乘除
#include <stdio.h>
int main()
{
char s1[] = { 'H', 'e', 'l', 'l', 'o' };
int i = 0;
char s2[] = { 'W', 'o', 'r', 'l', 'd' };
char* p0 = s1; // 数组s1首元素地址赋值给p0
char* p1 = &s1[3]; // 数组s1第四个元素地址赋值给p1
char* p2 = s2; // 数组s2首元素地址赋值给p2
int* p = &i; // 变量i的地址赋值给p
printf("%d\n", p0 - p1); // -3
// printf("%d\n", p0 + p2); // ERROR 不指向同一个数组没意义
// printf("%d\n", p0 - p2); // ERROR 不指向同一个数组没意义
// printf("%d\n", p0 - p); // ERROR 不指向同一个数组没意义
// printf("%d\n", p0 * p2); // ERROR
// printf("%d\n", p0 / p2); // ERROR
return 0;
}
(3)实践:指针运算的应用
#include <stdio.h>
#define DIM(a) (sizeof(a) / sizeof(*a)) // 计算数组有多少元素
int main()
{
char s[] = { 'H', 'e', 'l', 'l', 'o' };
char* pBegin = s; // 数组的首元素地址
char* pEnd = s + DIM(s); // 数组的最后一个元素的地址+sizeof(*s)
char* p = NULL;
printf("pBegin = %p\n", pBegin); // 数组的首元素地址
printf("pEnd = %p\n", pEnd); // 数组的最后一个元素的地址+sizeof(*s)
printf("Size: %d\n", pEnd - pBegin);// 5 - 0 = 5
for (p = pBegin; p < pEnd; p++) // 打印每个字符
{
printf("%c", *p);
}
printf("\n");
return 0;
}
4、小结
- 数组声明时编译器自动分配一片连续的内存空间
- 指针声明时只分配了用于容纳地址值的4字节空间
- 指针和整数可以进行运算,其结果为指针
- 指针之间只支持减法运算,其结果为数组元素下标差
- 指针之间支持比较运算,其类型必须相同
四、指针和数组分析(下)
数组名可以当作常量指针使用,那么指针是否也可以当作数组名来使用呢?
1、数组的访问方式
以下标的形式访问数组中的元素
以指针的形式访问数组中的元素
2、下标形式VS 指针形式
(1)下标形式与指针形式的效率及转化问题
- 指针以固定增量在数组中移动时,效率高于下标形式
- 指针增量为1且硬件具有硬件增量模型时,效率更高
- 下标形式与指针形式的转换
注意:
现代编译器的生成代码优化率已大大提高,在固定增量时,下标形式的效率已经和指针形式相当;但从可读性和代码维护的角度来看,下标形式更优。
实践:数组的访问方式
(2)实践:数组的访问方式
#include <stdio.h>
int main()
{
int a[5] = {0};
int* p = a;
int i = 0;
for(i=0; i<5; i++)
{
p[i] = i + 1;
}
for(i=0; i<5; i++)
{
printf("a[%d] = %d\n", i, *(a + i));
}
printf("\n");
for(i=0; i<5; i++)
{
i[a] = i + 10; // i[a] <--> a[i]
}
for(i=0; i<5; i++)
{
printf("p[%d] = %d\n", i, p[i]);
}
return 0;
}
(3)实践:数组和指针不同
int a[] = {1, 2, 3, 4, 5};
#include <stdio.h>
int main()
{
extern int a[]; // 数组的形式
printf("&a = %p\n", &a); // 数组的地址
printf("a = %p\n", a); // 数组首元素的地址
printf("*a = %d\n", *a); // 数组首元素的值
return 0;
}
#include <stdio.h>
int main()
{
extern int* a; // 此处更换为指针的形式
printf("&a = %p\n", &a); // 数组的地址
printf("a = %p\n", a); // 数组首元素的地址
printf("*a = %d\n", *a); // 数组首元素的值
return 0;
}
3、a和&a的区别
(1)首元素地址和数组地址的区别
- a为数组首元素的地址
- &a为整个数组的地址
- a和&a的区别在于指针运算
(2)实践:指针运算经典问题
#include <stdio.h>
int main()
{
int a[5] = {1, 2, 3, 4, 5};
int* p1 = (int*)(&a + 1); // &a+1相当于跨越了整个数组,&a+sizeof(a)
int* p2 = (int*)((int)a + 1);// (int)a强制转换成int型,a + 1,再强制转换为int型指针
int* p3 = (int*)(a + 1); // &a+1相当于跳到下一个元素,&a+sizeof(*a)
printf("%d, %d, %d\n", p1[-1], p2[0], p3[1]); // 5 33554432 3
return 0;
}
在Linux里面为小端系统,内存表现形式是高字节保存在内存的高地址中。
所谓的小端模式(Little-endian),是指数据的高字节保存在内存的高地址中,而数据的低字节保存在内存的低地址中,这种存储模式将地址的高低和数据位权有效地结合起来,高地址部分权值高,低地址部分权值低,和我们的逻辑方法一致。
0x02000000(十六进制)==>33,554,432(十进制)
4、数组参数
(1)数组作为参数入参
- 数组作为函数参数时,编译器将其编译成对应的指针
结论:
一般情况下,当定义的函数中有数组参数时,需要定义另一个参数来标示数组的大小。
(2)实践:虚幻的数组参数
#include <stdio.h>
void func1(char a[5])
{
printf("In func1: sizeof(a) = %d\n", sizeof(a)); // 4 byte
*a = 'a'; // 赋值
a = NULL; // 修改地址 在此处编译无报错,说明传入的不是数组
}
void func2(char b[])
{
printf("In func2: sizeof(b) = %d\n", sizeof(b)); // 4 byte
*b = 'b'; // 赋值
b = NULL; // 修改地址 在此处编译无报错,说明传入的不是数组
}
int main()
{
char array[10] = {0};
func1(array);
printf("array[0] = %c\n", array[0]); // 打印 a
func2(array);
printf("array[0] = %c\n", array[0]); // 打印 b
return 0;
}
5、小结
- 数组名和指针仅使用方式相同
- 数组名并不是数组的地址,而是数组首元素的地址
- 函数的数组参数退化为指针
五、C语言中的字符串
1、字符串的概念
- 字符串是有序字符的集合
- 字符串是程序中的基本元素之一
- C语言中没有字符串的概念
- C语言中通过特殊的字符数组模拟字符串
- C语言中的字符串是以‘\0’结尾的字符数组
2、字符数组与字符串
(1)字符串的本质
- 在C语言中,双引号引用的单个或多个字符是一种特殊的字面量
- 存储于程序的全局只读存储区
- 本质为字符数组,编译器自动在结尾加上'\0'字符
(2)实践:字符数组与字符串
#include <stdio.h>
int main()
{
char ca[] = {'H', 'e', 'l', 'l', 'o'};
char sa[] = {'W', 'o', 'r', 'l', 'd', '\0'};
char ss[] = "Hello world!";
char* str = "Hello world!";
printf("%s\n", ca);// err 缺少\0
printf("%s\n", sa);
printf("%s\n", ss);
printf("%s\n", str);
return 0;
}
3、鲜为人知的小秘密
- 字符串字面量的本质是一个数组
- 字符串字面量可以看作常量指针
- 字符串字面量中的字符不可改变
- 字符串字面量至少包含一个字符
4、字符串字面量
(1)“Hello World ! ”是一个无名的字符数组
(2)实践:字符串字面量的本质
#include <stdio.h>
int main()
{
char b = "abc"[0]; // "abc"是一个无名的字符数组
char c = *("123" + 1); // "123"是一个无名的字符数组
char t = *""; // ""是一个无名的字符数组
printf("%c\n", b);
printf("%c\n", c);
printf("%d\n", t);
printf("%s\n", "Hello");
printf("%p\n", "World");
return 0;
}
5、字符串的长度
(1)字符串的长度
- 字符串的长度就是字符串所包含字符的个数
- 字符串长度指的是第一个‘\0’字符前出现的字符个数
- 通过‘\0′结束符来确定字符串的长度
- 函数strlen用于返回字符串的长度
(2)实践:strlen的使用
#include <stdio.h>
#include <string.h>
int main()
{
char s[] = "Hello\0world";
int i = 0;
printf( "%d\n", sizeof(s)/sizeof(char)); // 12个元素
for(i=0; i<sizeof(s)/sizeof(char); i++) // 循环打印12个元素
{
printf("%c\n", s[i]);
}
printf("%s\n", s); // 打印整个字符串 hello 遇到\0结束打印
printf( "%d\n", strlen(s) ); // 打印字符的数量 5
printf( "%d\n", strlen("123") );// 打印字符的数量 3
return 0;
}
6、小结
- C语言中通过字符数组模拟字符串
- C语言中的字符串使用‘\0’作为结束符
- 字符串字面量的本质为字符数组
- 字符串相关函数都依赖于结束符‘\0'
六、字符串典型问题分析
1、snprintf 函数
下面的程序输出什么,为什么?
实践
#include <stdio.h>
int main()
{
char buf[20] = {0};
char src[] = "hello %s";
snprintf(buf, sizeof(buf), src);
printf("buf = %s\n", buf);
return 0;
}
#include <stdio.h>
int main()
{
char buf[20] = {0};
char src[] = "hello %s";
snprintf(buf, sizeof(buf), src,"Joker");
printf("buf = %s\n", buf);
return 0;
}
snprintf 函数本身是可变参数函数,原型如下︰
当函数只有3个参数时,如果第三个参数没有包含格式化信息,函数调用没有问题;相反,如果第三个参数包含了格式化信息,但缺少后续对应参数,则程序行为不确定。
2、结束符'\0'
下面的程序输出什么,为什么?
实践
#include <stdio.h>
#include <string.h>
int main()
{
#define STR "Hello, \0Joker\0"
char* src = STR; // 类似于 char* src = "Hello, \0Joker\0";
char buf[255] = {0};
snprintf(buf, sizeof(buf), src);// char buf[255] = "Hello, “;
printf("strlen(STR) = %d\n", strlen(STR)); // 7 Hello, (这个有个空格)
printf("sizeof(STR) = %d\n", sizeof(STR)); // 15 Hello, \0Joker\0\0
printf("strlen(src) = %d\n", strlen(src)); // 7 Hello, (这个有个空格)
printf("sizeof(src) = %d\n", sizeof(src)); // 4 指针所占大小
printf("strlen(buf) = %d\n", strlen(buf)); // 7 Hello, (这个有个空格)
printf("sizeof(buf) = %d\n", sizeof(buf)); // 255 255 byte
printf("src = %s\n", src); // Hello, (这个有个空格)
printf("buf = %s\n", buf); // Hello, (这个有个空格)
return 0;
}
- 字符串相关的函数均以第一个出现的‘\0’作为结束符
- 编译器总是会在字符串字面量的末尾添加‘\0'
- 字符串字面量的本质为数组
3、strcmp函数
下面的程序输出什么,为什么?
#include <stdio.h>
#include <string.h>
int main()
{
#define S1 "Joker"
#define S2 "Joker"
if( S1 == S2 ) // 编译器不同得到的结果不同
{
printf("Equal\n");
}
else
{
printf("Non Equal\n");
}
if( strcmp(S1, S2) == 0 ) // Equal
{
printf("Equal\n");
}
else
{
printf("Non Equal\n");
}
return 0;
}
对于BCC而言,则为
- 字符串之间的相等比较需要用strcmp完成
- 不可直接用==进行字符串直接的比较
- 完全相同的字符串字面量的==比较结果为false
- 一些现代编译器能够将相同的字符串字面量映射到同一个无名字符数组,因此==比较结果为true 。
4、字符串循环右移
void right_shift_r(const char* src, char* result, unsigned int n);
函数功能:
将输入字符串src循环右移n位,result为输出结果。
要求:
以效率最高的方式实现。
示例:
"abcde" -- 2 --> "deabc"
"abcde" -- 8 --> "cdeab"
#include <stdio.h>
#include <string.h>
void right_shift_r(const char* src, char* result, unsigned int n)
{
const unsigned int LEN = strlen(src); // 计算传入字符串的长度
int i = 0;
for(i=0; i < LEN; i++) // 遍历所有字符
{
result[(n + i) % LEN] = src[i]; // 移动后的编号 =(当前编号+移动位数)% 数据长度
}
result[LEN] = '\0'; // 最后结尾 + \0
}
int main()
{
char result[255] = {0};
right_shift_r("abcde", result, 2); // 测试
printf("%s\n", result);
right_shift_r("abcde", result, 5); // 测试
printf("%s\n", result);
right_shift_r("abcde", result, 8); // 测试
printf("%s\n", result);
return 0;
}
七、数组指针和指针数组分析
1、数组类型
- C语言中的数组有自己特定的类型
- 数组的类型由元素类型和数组大小共同决定
2、定义数组类型
- C语言中通过typedef 为数组类型重命名
- typedef type(name)[size];
- 数组类型∶
- typedef int(AINT5)[5];
- typedef float(AFLOAT10)[10];
- 数组定义∶
- AINT5 iArray;
- AFLOAT10 fArray;
3、数组指针
(1)数组指针的定义
- 数组指针用于指向一个数组
- 数组名是数组首元素的起始地址,但并不是数组的起始地址
- 通过将取地址符&作用于数组名可以得到数组的起始地址
- 可通过数组类型定义数组指针:ArrayType* pointer;
- 也可以直接定义: type(*pointer)[n];
- pointer为数组指针变量名
- type为指向的数组的元素类型
- n为指向的数组的大小
(2)实践:数组类型和数组指针
#include <stdio.h>
typedef int(AINT5)[5];
typedef float(AFLOAT10)[10];
typedef char(ACHAR9)[9];
int main()
{
AINT5 a1;
float fArray[10];
AFLOAT10* pf = &fArray; // 定义 包含10个元素的float型数组指针
ACHAR9 cArray; // 定义 包含9个元素的char型数组
char(*pc)[9] = &cArray; // 定义 包含9个元素的char型数组指针
char(*pcw)[4] = cArray; // err 不是统一的类型
int i = 0;
printf("%d, %d\n", sizeof(AINT5), sizeof(a1)); // AINT sizeof(int)* 5 = 20 ;a1 是包含5个元素的int型数组,也是20
for(i=0; i<10; i++)
{
(*pf)[i] = i; // 相当于 fArray[i] = i;
}
for(i=0; i<10; i++)
{
printf("%f\n", fArray[i]); // 打印 0 - 9
}
printf("%p, %p, %p\n", &cArray, pc+1, pcw+1); // &cArray 与&cArray相差9(sizeof(*pc) => sizeof(char)* 9)
// 与&cArray相差4(sizeof(*pcw) => sizeof(char)* 4)
return 0;
}
4、指针数组
(1)指针数组的定义
- 指针数组是一个普通的数组
- 指针数组中每个元素为一个指针
- 指针数组的定义: type* pArray[n];
- type*为数组中每个元素的类型
- pArray为数组名
- n为数组大小
(2)实践:指针数组的应用
#include <stdio.h>
#include <string.h>
#define DIM(a) (sizeof(a)/sizeof(*a)) // 计算数组中的元素数量
// 在数组中找到某个字符串的位置
int lookup_keyword(const char* key, const char* table[], const int size)
{ // 想要寻找的字符串 存放字符串的指针数组 数组的大小
int ret = -1;
int i = 0;
for(i=0; i<size; i++) // 遍历所有元素
{
if( strcmp(key, table[i]) == 0 ) // 字符串对比
{
ret = i;
break;
}
}
return ret;
}
int main()
{
const char* keyword[] = { // 利用指针数组定义多个字符串元素
"do",
"for",
"if",
"register",
"return",
"switch",
"while",
"case",
"static"
};
printf("%d\n", lookup_keyword("return", keyword, DIM(keyword)));
printf("%d\n", lookup_keyword("main", keyword, DIM(keyword)));
return 0;
}
5、小结
- 数组的类型由元素类型和数组大小共同决定
- 数组指针是一个指针,指向对应类型的数组
- 指针数组是一个数组,其中每个元素都为指针
- 数组指针遵循指针运算法则
- 指针数组拥有C语言数组的各种特性
八、main函数与命令行参数
1、main函数的概念
(1)main函数的概念
- C语言中main函数称之为主函数
- 一个C程序是从main函数开始执行的
2、main函数的本质
(1)main函数的本质
- main函数是操作系统调用的函数
- 操作系统总是将main函数作为应用程序的开始
- 操作系统将main函数的返回值作为程序的退出状态
思考∶
为什么C编译器支持那么多不同的main函数原型?
在以前系统单一,main函数是否有返回值都不重要,所以形式很多。现在的编译器为了满足绝大多数情况,都包含在内了。
3、main函数的参数
(1)main函数的参数
- 程序执行时可以向main函数传递参数
- gcc编译器的常见用法:gcc a.c b.c c.c
(2)实践:main函数的参数
#include <stdio.h>
int main(int argc, char* argv[], char* env[])
{
int i = 0;
printf("============== Begin argv ==============\n");
for(i=0; i<argc; i++)
{
printf("%s\n", argv[i]);
}
printf("============== End argv ==============\n");
printf("\n");
printf("\n");
printf("\n");
printf("============== Begin env ==============\n");
for(i=0; env[i]!=NULL; i++)
{
printf("%s\n", env[i]);
}
printf("============== End env ==============\n");
return 0;
}
4、小技巧
(1)面试中的小问题
main函数一定是程序执行的第一个函数吗?
不一定,使用GCC编译器的属性关键字,可以在main前或者后执行函数,在BCC没有属性关键字,就不可以。
(2)实践:gcc中的属性关键字
#include <stdio.h>
#ifndef __GNUC__
#define __attribute__(x)
#endif
__attribute__((constructor))
void before_main()
{
printf("%s\n",__FUNCTION__);
}
__attribute__((destructor))
void after_main()
{
printf("%s\n",__FUNCTION__);
}
int main()
{
printf("%s\n",__FUNCTION__);
return 0;
}
5、小结
- 一个C程序是从main函数开始执行的
- main函数是操作系统调用的函数
- main函数有参数和返回值
- 现代编译器支持在main函数前调用其它函数
九、多维数组和多维指针
1、指向指针的指针
- 指针的本质是变量
- 指针会占用一定的内存空间
- 可以定义指针的指针来保存指针变量的地址值
2、为什么需要指向指针的指针?
- 指针在本质上也是变量
- 对于指针也同样存在传值调用与传址调用
实践:重置动态空间大小
#include <stdio.h>
#include <malloc.h>
// 扩大、缩小申请的动态内存
int reset(char**p, int size, int new_size)
{
int ret = 1;
int i = 0;
int len = 0;
char* pt = NULL;
char* tmp = NULL;
char* pp = *p; // 获取原申请内存的地址
if( (p != NULL) && (new_size > 0) )
{
pt = (char*)malloc(new_size); // 重新申请一块新的内存
tmp = pt; // 赋值给 tmp 进行后续操作,为了保护数据pt
len = (size < new_size) ? size : new_size; // 当前申请的大小与新申请的进行对比
for(i=0; i<len; i++)
{
*tmp++ = *pp++; // 将原来的数据赋值过去
}
free(*p); // 释放原来申请的内存
*p = pt; // 将新地址传递出去
}
else
{
ret = 0;
}
return ret;
}
int main()
{
char* p = (char*)malloc(5); // 申请5字节动态内存
printf("%p\n", p); // 打印这块内存的地址
if( reset(&p, 5, 3) ) // 重新申请内存 - 原5字节,现缩小为3字节
{ // 传入的是指针p的地址,所以是指针的指针,指针的传址调用
printf("%p\n", p);// 打印更新后内存的地址
}
free(p); // 释放这块内存
return 0;
}
3、二维数组与二级指针
- 二维数组在内存中以一维的方式排布
- 二维数组中的第一维是一维数组
- 二维数组中的第二维才是具体的值
- 二维数组的数组名可看做常量指针
实践:遍历二维数组
#include <stdio.h>
#include <malloc.h>
void printArray(int a[], int size)
{
int i = 0;
printf("printArray: %d\n", sizeof(a)); // 指针占4字节内存
for(i=0; i<size; i++) // 遍历打印每个元素
{
printf("%d\n", a[i]); // 传入的是00位置的地址,同时二维数组也是像一维数组一样排放。
}
}
int main()
{
int a[3][3] = {{0, 1, 2}, {3, 4, 5}, {6, 7, 8}};
int* p = &a[0][0]; // 取二位数组的00位置的元素地址
int i = 0;
int j = 0;
for(i=0; i<3; i++)
{
for(j=0; j<3; j++)
{
printf("%d, ", *(*(a+i) + j)); // *(*(a+i) + j)) = > *(a[i] + j)) = > a[i][j]
}
printf("\n");
}
printf("\n");
printArray(p, 9); // 传入的是00位置的元素地址,遍历打印数组中的数据
return 0;
}
*(*(a+i) + j)) = > *(a[ i ] + j)) = > a[ i ][ j ]
a + i * ( 3 * sizeof(int)) + j * sizeof(int)
4、数组名
(1)一维数组和二维数组及其数组名
- —维数组名代表数组首元素的地址
- 二维数组名同样代表数组首元素的地址
- int m[2][5] m的类型为int(*)[5]
结论︰
- 1.二维数组名可以看做是指向数组的常量指针
- 2.二维数组可以看做是一维数组
- 3.二维数组中的每个元素都是同类型的一维数组
(2)实践:如何动态申请二维数组
#include <stdio.h>
#include <malloc.h>
int** malloc2d(int row, int col)
{
int** ret = NULL;
if( (row > 0) && (col > 0) )
{
int* p = NULL;
ret = (int**)malloc(row * sizeof(int*)); // 申请一个一维数组,里面每个元素是int*,row个,有多少行
p = (int*)malloc(row * col * sizeof(int)); // 二维数组在内存里排列方式和一维数组一样,直接申请一个一维数组,行*列个元素
if( (ret != NULL) && (p != NULL) ) // 映射关系
{
int i = 0;
for(i=0; i<row; i++)
{
ret[i] = p + i * col; // 将刚才申请的每一行的指针 指向 刚才申请出来的对应的元素
}
}
else
{
free(ret);
free(p);
ret = NULL;
}
}
return ret;
}
void free2d(int** p) // 释放内存
{
if( *p != NULL )
{
free(*p);
}
free(p);
}
int main()
{
int** a = malloc2d(3, 3); // 3*3的二维数组
int i = 0;
int j = 0;
for(i=0; i<3; i++)
{
for(j=0; j<3; j++)
{
printf("%d, ", a[i][j]); // 打印每个数据
}
printf("\n");
}
free2d(a);
return 0;
}
5、小结
- C语言中只支持一维数组
- C语言中的数组大小必须在编译期就作为常数确定
- C语言中的数组元素可是任何类型的数据
- C语言中的数组的元素可以是另一个数组
十、数组参数和指针参数分析
为什么C语言中的数组参数会退化为指针?
1、退化的意义
- C语言中只会以值拷贝的方式传递参数
- 当向函数传递数组时:
(X)将整个数组拷贝一份传入函数
(√ )将数组名看做常量指针传数组首元素地址
C语言以高效作为最初设计目标:
- a) 参数传递的时候如果遍历拷贝整个数组执行效率将大大下降。
- b) 参数位于栈上,太大的数组拷贝将导致栈溢出。(函数之所以能被调用,因为有函数调用栈,是计算机的一片内存,有大小限制。函数参数也会都拷贝到栈上,如果过大就会溢出。)
2、二维数组参数
- 二维数组参数同样存在退化的问题
- 二维数组可以看做是一维数组
- 二维数组中的每个元素是一维数组
- 二维数组参数中第一维的参数可以省略
3、等价关系
4、被忽视的知识点
- C语言中无法向一个函数传递任意的多维数组
- 必须提供除第一维之外的所有维长度
- 第一维之外的维度信息用于完成指针运算
- N维数组的本质是一维数组,元素是N-1维的数组
- 对于多维数组的函数参数只有第一维是可变的
实践:传递与访问二维数组
#include <stdio.h>
void access(int a[][3], int row)
{
int col = sizeof(*a) / sizeof(int);
int i = 0;
int j = 0;
printf("sizeof(a) = %d\n", sizeof(a));
printf("sizeof(*a) = %d\n", sizeof(*a));
for(i=0; i<row; i++)
{
for(j=0; j<col; j++)
{
printf("%d\n", a[i][j]);
}
}
printf("\n");
}
void access_ex(int b[][2][3], int n)
{
int i = 0;
int j = 0;
int k = 0;
printf("sizeof(b) = %d\n", sizeof(b));
printf("sizeof(*b) = %d\n", sizeof(*b));
for(i=0; i<n; i++)
{
for(j=0; j<2; j++)
{
for(k=0; k<3; k++)
{
printf("%d\n", b[i][j][k]);
}
}
}
printf("\n");
}
int main()
{
int a[3][3] = {{0, 1, 2}, {3, 4, 5}, {6, 7, 8}};
int aa[2][2] = {0};
int b[1][2][3] = {0};
access(a, 3);
access(aa, 2);
access_ex(b, 1);
access_ex(aa, 2);
return 0;
}
5、小结
- C语言中只会以值拷贝的方式传递参数
- C语言中的数组参数必然退化为指针
- 多维数组参数必须提供除第一维之外的所有维长度
- 对于多维数组的函数参数只有第一维是可变的
十一、函数与指针分析
1、函数类型
- C语言中的函数有自己特定的类型
- 函数的类型由返回值,参数类型和参数个数共同决定
- C语言中通过typedef为函数类型重命名
2、函数指针
- 函数指针用于指向一个函数
- 函数名是执行函数体的入口地址
- 可通过函数类型定义函数指针: FuncType* pointer;
- 也可以直接定义: type (*pointer)( parameter list);
- pointer为函数指针变量名
- type为所指函数的返回值类型
- parameter list为所指函数的参数类型列表
面试小问题:如何使用C语言直接跳转到某个固定的地址开始执行?
实践:函数指针的使用
3、回调函数
- 回调函数是利用函数指针实现的一种调用机制
- 回调机制原理
- 调用者不知道具体事件发生时需要调用的具体函数
- 被调函数不知道何时被调用,只知道需要完成的任务
- 当具体事件发生时,调用者通过函数指针调用具体函数
- 回调机制中的调用者和被调函数互不依赖
实践:回调函数使用示例
4、小结
- C语言中的函数都有特定的类型
- 可以使用函数类型定义函数指针
- 函数指针是实现回调机制的关键技术
- 通过函数指针可以在C程序中实现固定地址跳
十二、指针阅读技巧分析
1、笔试中的问题
下面的标识符代表什么含义?
2、指针阅读技巧解析
- 右左法则
- 从最里层的圆括号中未定义的标示符看起
- 首先往右看,再往左看
- 遇到圆括号或方括号时可以确定部分类型,并调转方向
- 重复2,3步骤,直到阅读结束
实践:复杂指针的阅读
3、小结
- 右左法则总结于编译器对指针变量的解析过程
- 指针阅读练习的意义在于理解指针的组合定义
- 可通过typedef 简化复杂指针的定义
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)