目录
一、指针的本质分析
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、小结
int i = 0;
int j = 0;
//指针声明
int* p = &i;
//取值
j = *p;
//赋值
*p = 10;
图解如下:
内存是由字节组成的,每个字节都有一个编号。指针变量主要是存放相同数据类型的变量的首地址。这里的这个地址其实就是内存的某个字节的编号。而这个编号的确定是与地址总线有关。如果地址总线是32位,则它的寻址范围是0~2^32(0~4G)。那么为一个字节的编址就会由32个0或者1组成。例如第一个字节的编址是32个0,最后一个的编址是32个1。一个字节有8位,32位则需要4个字节。
简单的说32位的操作系统就是指:地址总线是32位的系统。那么,也就是说操作系统的位数决定了指针变量所占的字节数。
#include
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;
}
① 传值调用:这部分代码并不能够实现交换变量的值,这就是传值调用。
// 这部分代码并不能够实现交换变量的值,这就是传值调用
#include
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
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;
}
const int* p; // p可变,p指向的内容不可变
int const* p; // p可变,p指向的内容不可变
int*const p; // p不可变,p指向的内容可变
const int* const p; // p和p指向的内容都不可变
方法:不看数据类型,如果是*p代表指向的地址的数值不能变,如果是p代表地址不能变。
口诀∶左数右指
#include
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;
}
#include
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;
}
#include
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;
}
#include
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;
}
#include
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;
}
结论∶
当指针p指向一个同类型的数组的元素时:p+1将指向当前元素的下一个元素;p-1将指向当前元素的上一个元素。
注意∶
1、 只有当两个指针指向同一个数组中的元素时,指针相减才有意义,其意义为指针所指元素的下标差
2、 当两个指针指向的元素不在同一个数组中时,结果未定义
#include
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;
}
#include
#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;
}
数组名可以当作常量指针使用,那么指针是否也可以当作数组名来使用呢?
注意:
现代编译器的生成代码优化率已大大提高,在固定增量时,下标形式的效率已经和指针形式相当;但从可读性和代码维护的角度来看,下标形式更优。
实践:数组的访问方式
#include
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;
}
int a[] = {1, 2, 3, 4, 5};
#include
int main()
{
extern int a[]; // 数组的形式
printf("&a = %p\n", &a); // 数组的地址
printf("a = %p\n", a); // 数组首元素的地址
printf("*a = %d\n", *a); // 数组首元素的值
return 0;
}
#include
int main()
{
extern int* a; // 此处更换为指针的形式
printf("&a = %p\n", &a); // 数组的地址
printf("a = %p\n", a); // 数组首元素的地址
printf("*a = %d\n", *a); // 数组首元素的值
return 0;
}
#include
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(十进制)
结论:
一般情况下,当定义的函数中有数组参数时,需要定义另一个参数来标示数组的大小。
#include
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;
}
#include
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;
}
#include
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;
}
#include
#include
int main()
{
char s[] = "Hello\0world";
int i = 0;
printf( "%d\n", sizeof(s)/sizeof(char)); // 12个元素
for(i=0; i
实践
#include
int main()
{
char buf[20] = {0};
char src[] = "hello %s";
snprintf(buf, sizeof(buf), src);
printf("buf = %s\n", buf);
return 0;
}
#include
int main()
{
char buf[20] = {0};
char src[] = "hello %s";
snprintf(buf, sizeof(buf), src,"Joker");
printf("buf = %s\n", buf);
return 0;
}
当函数只有3个参数时,如果第三个参数没有包含格式化信息,函数调用没有问题;相反,如果第三个参数包含了格式化信息,但缺少后续对应参数,则程序行为不确定。
实践
#include
#include
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;
}
#include
#include
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而言,则为
void right_shift_r(const char* src, char* result, unsigned int n);
函数功能:
将输入字符串src循环右移n位,result为输出结果。
要求:
以效率最高的方式实现。
示例:
"abcde" -- 2 --> "deabc"
"abcde" -- 8 --> "cdeab"
#include
#include
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;
}
#include
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;
}
#include
#include
#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
思考∶
为什么C编译器支持那么多不同的main函数原型?
在以前系统单一,main函数是否有返回值都不重要,所以形式很多。现在的编译器为了满足绝大多数情况,都包含在内了。
#include
int main(int argc, char* argv[], char* env[])
{
int i = 0;
printf("============== Begin argv ==============\n");
for(i=0; i
main函数一定是程序执行的第一个函数吗?
不一定,使用GCC编译器的属性关键字,可以在main前或者后执行函数,在BCC没有属性关键字,就不可以。
#include
#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;
}
#include
#include
// 扩大、缩小申请的动态内存
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
#include
#include
void printArray(int a[], int size)
{
int i = 0;
printf("printArray: %d\n", sizeof(a)); // 指针占4字节内存
for(i=0; i *(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)
结论︰
#include
#include
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|
为什么C语言中的数组参数会退化为指针?
C语言以高效作为最初设计目标:
#include
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|
面试小问题:如何使用C语言直接跳转到某个固定的地址开始执行?
下面的标识符代表什么含义?
实践:复杂指针的阅读