一.《第26课 - 指针的本质分析》
1. 变量回顾
程序中的变量只是一段存储空间的别名,那么是不是必须通过这个别名才能使用这段内存空间?
2. * 号的意义
(1)在指针声明时,*号表示所声明的变量为指针
(2)在指针使用时,*号表示取指针所指向的内存空间中的值
3. 传值调用与传址调用
(1)指针是变量,因此可以声明指针参数
(2)当一个函数体内部需要改变实参的值,则需要使用指针参数
(3)函数调用时实参值将复制到形参
(4)指针适用于复杂数据类型作为参数的函数中
4. 常量与指针
(1)
二.《第27课 - 数组的本质分析》
1. 数组的概念
数组是相同类型的变量的有序集合。
下图是一个包含5个int类型数据的数组的示意图:
2. 数组的大小
(1)数组在一片连续的内存中间存储元素
(2)数组元素的个数可以显示的或隐式的指定
观察下面两个数组,思考两个问题:① a[2],a[3],a[4] 的值是多少? ② b包含了多少个元素?
【数组的初始化】
#include <stdio.h>
int main()
{
int a[5] = {1, 2}; // 编译器将未初始化的元素初始化为0
int b[] = {1, 2}; // 数组未指定大小 ==> 编译器根据数组初始化的情况计算其大小(占用的空间最小)
printf("a[2] = %d\n", a[2]); // 0
printf("a[3] = %d\n", a[3]); // 0
printf("a[4] = %d\n", a[4]); // 0
// sizeof(数组名) ==> 计算数组的大小
printf("sizeof(a) = %zu\n", sizeof(a)); // 20
printf("sizeof(b) = %zu\n", sizeof(b)); // 8
printf("count for a: %zu\n", sizeof(a)/sizeof(a[0])); // 5
printf("count for b: %zu\n", sizeof(b)/sizeof(b[0])); // 2
return 0;
}
3. 数组地址与数组名
(1)数组名代表数组首元素的地址
(2)数组的地址需要对数组名使用取地址符 & 才能得到
(3)数组首元素的地址值与数组的地址值相同,但是数组首元素的地址与数组的地址是两个不同的概念。
我们通常所说的地址包括两个方面的概念:起始地址 和 所占用内存空间的大小。数组名仅仅对应数组一个元素那么大的内存,而数组的地址对应整个数组所占用的内存空间。
【数组名和数组地址】
#include <stdio.h>
int main()
{
int a[5] = {0};
printf("a = %p\n", a); // 0x7ffc7425dbb0
printf("&a = %p\n", &a); // 0x7ffc7425dbb0
printf("&a[0] = %p\n", &a[0]); // 0x7ffc7425dbb0
return 0;
}
4. 数组名的盲点
(1)数组名可以看作一个指针常量;注意这里只是说"可以看作",并非是真正的指针常量
(2)数组名"指向"的是内存中数组首元素的起始位置
(3)数组名不包含数组的长度信息
(4)在表达式中数组名只能作为右值使用
(5)只有在下列场合中数组名不能看作常量指针
- 数组名作为作为 sizeof 操作符的参数,表示整个数组的大小
- 数组名作为 & 运算符的参数,表示整个数组的地址
【数组和指针并不相同】
#include <stdio.h>
int main(){
int a[5] = {0};
int b[2];
int* p = NULL;
p = a;
printf("a = %p\n", a); // 0x7ffe40054a70, 数组a首元素的地址
printf("p = %p\n", p); // 0x7ffe40054a70
printf("&p = %p\n", &p); // 指针p的地址, 0x7ffe40054a68
printf("sizeof(a) = %zu\n", sizeof(a)); // 数组a的大小, 20
printf("sizeof(p) = %zu\n", sizeof(p)); // 指针的大小为8(64位系统)
printf("\n");
p = b;
printf("b = %p\n", b); // 0x7ffe40054a60, 数组b首元素的地址
printf("p = %p\n", p); // 0x7ffe40054a60
printf("&p = %p\n", &p); // 指针p的地址, 0x7ffe40054a68
printf("sizeof(b) = %zu\n", sizeof(b)); //数组的大小, 8
printf("sizeof(p) = %zu\n", sizeof(p)); //指针的大小为8(64位系统)
// a = b; // compile error, 数组名不能作为左值
// error: incompatible types when assigning to type ‘int[5]’ from type ‘int *’
return 0;
}
三.《第28课 - 指针和数组分析(上)》
1. 数组的本质
(1)数组是一段连续的内存空间
(2)数组的大小为 sizeof(array_type) * array_size
(3)数组名可以看作指向数组第一个元素的指针常量
思考下面两个问题:
① a + 1的意义是什么?结果又是什么?
② 指针运算的意义是什么?结果又是什么?
【a+1的结果是什么?】
#include <stdio.h>
int main()
{
int a[5] = {0};
int *p = NULL;
printf("a = 0x%lX\n", (unsigned long)a); // a = 0x7FFF043D43A0
printf("a + 1 = 0x%lX\n", (unsigned long)(a + 1)); // a + 1 = 0x7FFF043D43A4,a[1]的地址
printf("p = 0x%lX\n", (unsigned long)p); // p = 0x0
printf("p + 1 = 0x%lX\n", (unsigned long)(p + 1)); // p + 1 = 0x4
return 0;
}
2. 指针的运算
(1)指针是一种特殊的变量,与整数的运算规则为:
当指针p指向一个同类型的数组元素时,p + 1将指向当前元素的下一个元素,p -1指向当前元素的上一个元素。
(2)指针之间只支持减法运算,参与减法运算的指针类型必须相同。
对于数组而言,只有当两个指针指向同一个数组中的元素时,指针相减才有意义,其意义为指针所指元素的下标差。当两个指针指向的元素不在同一个数组时,结果未定义。
3. 指针的比较
(1)指针也可以进行关系运算(< <= > >= == !=)
(2)参与关系运算的指针类型必须相同
(3)指针关系运算的前提是:必须指向同一个数组中的元素
【指针运算初探】
#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;
char* p1= &s1[3];
char* p2 = s2;
int* p = &i;
printf("%ld\n", p0 - p1); // -3
// printf("%ld\n", p0 + p2); // compile error,指针之间不允许加法运算
printf("%ld\n", p0 - p2); // 语法正确,但是p0和p2指向两个不同的数组,结果没有意义
// printf("%ld\n", p0 - p); // compile error,不同类型指针之间不能相减
// printf("%ld\n", p0 * p2); // compile error,指针之间不允许乘法运算
// printf("%ld\n", p0 / p2); // compile error,指针之间不允许除法运算
return 0;
}
【指针运算的应用】
#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); // 数组末尾的后一个地址
char* p = NULL;
printf("pBegin = %p\n", pBegin);
printf("pEnd = %p\n", pEnd);
printf("Size: %ld\n", pEnd - pBegin); // 5
for (p = pBegin; p < pEnd; p++) // Hello
{
printf("%c",*p);
}
printf("\n");
return 0;
}
四.《第29课 - 指针和数组分析(下)》
1. 数组的访问方式
(1)访问数组元素有两种方式:以下标的形式访问数组中的元素 和 以指针的形式访问数组中的元素。
(2)下标形式 VS 指针形式
- 指针以固定增量在数组中移动时,效率高于下标形式
- 指针增量为1且硬件具有硬件增量模型时效率更高
- 下标形式与指针形式的转换
※※ 注意:现代编译器的生成代码优化率已大大提高,在固定增量时,下标形式的效率已经和指针形式相当,但从可读性和代码维护的角度来看,下标形式更优。
【数组的访问方式】
#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));
}
for(i=0; i<5; i++)
{
i[a] = i + 10; // 等价于a[i] = i + 10
}
for(i=0; i<5; i++)
{
printf("p[%d] = %d\n", i, p[i]);
}
return 0;
}
【数组和指针不同】
ext.c
int a[] = {1, 2, 3, 4, 5};
29-2.c
#include <stdio.h>
// extern int a[];
extern int *a;
int main()
{
printf("&a = %p\n", &a);
printf("a = %p\n", a);
printf("a[0] = %d\n", a[0]);
return 0;
}
// extern int a[];
// &a = 0x601040
// a = 0x601040
// a[0] = 1
// extern int *a;
// &a = 0x601040
// a = 0x200000001
// 段错误 (核心已转储)
2. a 和 &a 的区别
(1)a 为数组首元素的地址
(2)&a 为整个数组的地址
(3)a 和 &a 的区别在于指针运算,前者针对的是数组的元素,后者针对的是整个数组
【指针运算经典问题】
#include <stdio.h>
int main()
{
int a[5] = {1, 2, 3, 4, 5};
int* p1 = (int*)(&a + 1); // 指向数组元素5的后一个位置
int* p2 = (int*)((int)a + 1); // 指向数组 (起始地址 + 1字节) 处,这里是整数运算,不是指针运算
int* p3 = (int*)(a + 1); // 指向第2个元素
printf("%d\n", p1[-1]); // 5
printf("%d\n", p2[0]); // 33554432
printf("%d\n", p3[1]); // 2
return 0;
}
3. 数组参数
(1)数组作为函数参数时,编译器将其编译成对应的指针 // 数组长度信息没有意义,都是退化为指针
(2)一般情况下,当定义的函数中有数组参数时,需要定义另一个参数来标示数组的大小
【虚幻的数组参数】
#include <stdio.h>
void func1(char a[5]) // char a[5] ==> char *a
{
printf("In func1:sizeof(a) = %d\n", sizeof(a)); // 8 ==> a退化为指针
*a = 'a';
a = NULL; // a可以作为左值,证明不是数组
}
void func2(char b[]) // 数组长度有无没有关系,char b[] ==> char *b
{
printf("In func2:sizeof(b) = %d\n", sizeof(b)); // 8 ==> b退化为指针
*b = 'b';
b = NULL; // a可以作为左值,证明不是数组
}
int main(){
char array[10] = {0};
func1(array);
printf("array[0] = %c\n", array[0]); // array[0] = a;
func2(array);
printf("array[0] = %c\n", array[0]); // array[0] = b;
return 0;
}
五.《第30课 - C语言中的字符串》
1. 字符串的概念
(1)字符串是有序字符的集合
(2)字符串是程序中的基本元素之一
(3)C语言中没有字符串的概念
- C语言中通过特殊的字符数组模拟字符串
- C语言中的字符串是以 '\0' 结尾的字符数组 // 回忆前面学过的转义符 \ ,'\0' 即八进制的0表示的字符,八进制的0在内存中就是0
2. 字符数组与字符串
(1)在C语言中,双引号引用的单个或多个字符是一种特殊的字面量
- 存储于程序的全局只读存储区
- 本质为字符数组,编译器自动在结尾加上 '\0' 字符
【字符数组与字符串】
#include <stdio.h>
int main()
{
char ca[] = {'H','e','l','l','o'};
char sa[] = {'W','o','r','l','d','\0'};
char ss[] = "Hello world!"; // 使用"Hello World!"这个字符数组去初始化ss数组,这种语法是允许的!!!
char* str = "Hello World!"; // 全局只读存储区,str[0]='h'是非法的
printf("%s\n", ca); // 打印Hello及其后面的内容,直到遇到 '\0'
printf("%s\n", sa); // World
printf("%s\n", ss); // Hello World!
printf("%s\n",str); // Hello World!
return 0;
}
3. 字符串字面量
(1)字符串字面量的本质是一个字符数组
(2)字符串字面量可以看作指针常量
(3)字符串字面量存储在程序的全局只读存储区,不可对其中的字符进行修改
(4)字符串字面量至少包含一个字符,即空字符串 "" 包含一个 '\n'
【字符串字面量的本质】
#include <stdio.h>
int main()
{
// 字符串字面量经过编译器编译,返回的是存储它的字符数组的地址,该字符数组村粗在程序的全局只读存储区
char b = "abc"[0];
char c = *("123" + 1);
char t = *"";
printf("%c\n", b); // a
printf("%c\n", c); // 2
printf("%d\n", t); // 0
printf("%s\n", "Hello"); // Hello
printf("%p\n", "World"); // 0x400692,存储该字符串字面量的字符数组的地址
return 0;
}
4. 字符串的长度
(1)字符串的长度指的就是第一个 '\0' 字符前出现的字符的个数
(2)函数 strlen 用于返回字符串的长度
【strlen的使用】
#include <stdio.h>
#include <string.h>
int main()
{
char s[] = "Hello\0World"; // 以第一个出现的'\0'作为字符串的结束符
int i = 0;
for (i=0; i<sizeof(s)/sizeof(s[0]); i++) {
printf("%c\n", s[i]);
}
printf("%zu\n", strlen(s)); // 5
printf("%zu\n", strlen("123")); // 3
return 0;
}
六.《第31课 - 字符串典型问题分析》
七.《第32课 - 数组指针和指针数组分析》
1. 数组的类型
C语言中的数组有自己特定的类型,数组的类型由 元素类型 和 数组大小 共同决定。比如 int array[5] 的类型就是 int[5];float farray[10] 的类型就是 float[10]。
2. 定义数组类型
C语言中通过typedef为数组类型重命名:
typedef type (name)[size];
使用这种方式重命名如下两个数组
typedef int (AINT5)[5]; typedef float (AFLOAT10)[10];
那么就可以使用 AINT5 和 AFLOAT10 定义数组,AINT5 iArray; AFLOAT10 fArray // iArray、fArray是数组名
3. 数组指针
(1)数组指针,顾名思义就是一个指针,只不过该指针用于指向一个数组
(2)前面学习数组时说过,数组名是数组首元素的地址,并不是整个数组的地址,使用&作用于数组名可以得到数组的地址
(3)定义数组指针的三种方式:
① 可以通过数组类型定义数组指针:ArrayType *pointer;
② 可以直接定义:type (*pointer)[n]; // pointer为数组指针变量名,type为数组元素的类型,n为数组的大小
③ 先使用typedef重命名数组指针类型typedef type (*POINTER)[n],然后使用该类型定义数组指针变量 POINTER pointer;
【数组类型和数组指针】
#include <stdio.h>
typedef int (AINT5)[5];
typedef float (AFLOAT10)[10];
typedef char (ACHAR9)[9];
int main()
{
AINT5 a1; // int a1[5]
float fArray[10];
AFLOAT10* pf = &fArray;
ACHAR9 cArray;
char(*pc)[9] = &cArray;
// char(*pcw)[4] = cArray; // 类型不匹配,数组名表示数组首元素的地址,对数组名&表示数组的地址
char(*pcw)[4] = (char(*)[4])cArray;
int i = 0;
printf("%zu, %zu\n", sizeof(AINT5), sizeof(a1)); // 20 20
for(i=0; i<10; i++)
{
(*pf)[i] = i; // fArray[i] = i;
}
printf("pf = %p, pf + 1 = %p\n", pf, pf+1); // float占4字节 pf = 0x7ffdb9e61e10, pf + 1 = 0x7ffdb9e61e38
for(i=0; i<10; i++)
{
printf("%f\n",fArray[i]); // 打印 0 到 9
}
// 0x7ffdc9e322a0, 0x7ffdc9e322a9, 0x7ffdc9e322a0 ,0x7ffdc9e322a4
printf("%p, %p, %p ,%p\n", &cArray, pc+1, pcw, pcw+1); // pc + 1 ==> (unsigned long)pc + 1*sizeof(*pc) ==> (unsigned long)pc + 9
// 同理 pcw + 1 = (unsigned long)pcw + 4
return 0;
}
4. 指针数组
(1)指针数组,顾名思义就是一个数组,该数组的元素类型为指针类型
(2)指针数组的定义:
type *PArray[n]
【指针数组的应用】
#include <stdio.h>
#include <string.h>
// DIM是维度(dimension)的缩写,一维数组的维度就是数组元素的个数
#define DIM(a) (sizeof(a)/sizeof(*a))
//table指向一个指针数组,即每个元素为指针类型
int lookup_keyword(const char* key, const char* table[], const int size) // ① 数组作为形参退化为指针类型 ② 需要传入数组长度信息
{ // char *table[] 等价于 char **table,但是后者看起来更加直观,推荐使用这种写法
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))); // 4
printf("%d\n", lookup_keyword("main", keyword, DIM(keyword))); // -1
return 0;
}