文章目录
- 🧀 I GET IT !
- 1、数组的容量:数组可以存储的元素个数
- 2、数组的大小:数组占用的内存空间(以字节为单位)
- 3、sizeof() 返回值:数组的大小
- 4、指针运算:自动根据指针类型计算偏移量
- 5、字符数组
- 6、指向字符串常量的字符指针
- 7、动态分配内存的字符指针
- 补充:str[0]等价于*(str + 0),无论str是数组名还是字符指针,都可以通过数组下标的方式来访问和修改内存里的内容(前提是没指向内存中只读区域)
- 8、不能取地址的常量表达式
- 9、用 const 声明的常量可以被取地址,但是,它只能由const声明的指针指向,除非强制类型转换来绕过检查,但是会导致未定义行为
- 10、函数标识符 = 函数名
- 11、整型和基本整型
- 12、直接递归是函数体内部调用自己,间接递归通过调用其他调用自己的函数间接调用自己
- 13、全局变量的定义与声明
- 14、C语言支持两种调用方式
🧀 I GET IT !
1、数组的容量:数组可以存储的元素个数
数组的容量在声明的时候就要给出,如果不给出容量的话,就必须对数组进行初始化。这个时候数组的容量会自动地根据初始化的元素的个数来确定。假如[]
里面是空的,初始化为{1,2,3}
,那默认这个数组的容量是3。
2、数组的大小:数组占用的内存空间(以字节为单位)
数组大小 = 容量 × 每个元素的大小
3、sizeof() 返回值:数组的大小
计算数组容量可以用公式:
容量 = sizeof(数组名) / sizeof(数组元素类型);
int a[3];
int capacity = sizeof(a) / sizeof(int); // 容量为3
4、指针运算:自动根据指针类型计算偏移量
- a[2] 等价于 *(a + 2)。
- 假设数组a的元素都是int型,*(a+2) 的偏移量会自动计算为 2 × sizeof(int) = 8字节
- p是一个指针,p+3 表示指针p向后移动3个 对象 。实际的偏移量还是 3 × sizeof(*p);p + 3 的新地址 = p的地址 + 3 × sizeof(*p);
5、字符数组
(1)初始化
在C语言中,字符数组的初始化有一些特殊的规则,允许我使用字符串字面量来初始化字符数组。这是因为字符串字面量在C语言中是以字符数组的形式存储的,并且编译器会自动处理字符串的存储和初始化。(建立临时字符数组,复制给自定义数组)。
char str[6] = "hello";///含 \0,刚好 6 字节
char str[6] = {"hello"};//语法糖,同上
编译器会做以下几件事情:
- 分配内存:为 str 分配 10 个字节的内存。
- 复制字符串:将字符串 “hello” 的内容(包括结尾的 \0)复制到 str 中。
- 填充剩余部分:如果字符串的长度小于数组的大小,剩余的字节会被初始化为 \0。
(2)内容可以修改,但数组名不可直接被赋值
- 字符数组的内容是可以修改的,因为数组的内存是可写的。
- 字符数组不能直接赋值,需要使用 strcpy 或类似函数来复制字符串。
str[0] = 'H';//合法
strcpy(str,"abcdog");
(3)字符数组大小固定,内存是静态分配的,在栈上,不需要手动释放。
6、指向字符串常量的字符指针
例子:
char *str = "hello";
存储方面:
- 字符串常量(如 “hello”)存储在程序的只读内存区域(如 .rodata 段)。
- 指针本身(如 char *str = “hello”;)通常分配在栈上(如果是局部变量)或全局内存区域(如果是全局变量)。
关于修改:指针可重新赋值,内容不可修改
- 字符串常量:存储在只读内存中,不能修改。
str[0] = 'H';
是非法操作,会导致未定义行为(通常是程序崩溃,一直不工作~)。 - 字符指针:可以被直接赋值,指向另一个字符串常量。
大小和长度:
- 字符指针的大小固定4或8字节;
- 字符串的长度由字符串常量决定,可以通过 strlen 获取。
char *str = "hello";
int len = strlen(str); // len = 5
7、动态分配内存的字符指针
例子:
char *str = malloc(10);
strcpy(str, "hello");
int len = strlen(str); // 实际字符串长度len = 5,\0都不算的
str[0] = 'H'; // 合法操作,将 "hello" 修改为 "Hello"
free(str); // 必须手动释放内存
存储方面:
- 使用 malloc、calloc 等函数动态分配内存,内存分配在堆上。
- 需要手动管理内存,使用 malloc 分配的内存必须显式调用 free 释放。
关于修改:
- 动态分配的内存是可写的,可以修改。
str[0] = 'H';
是合法操作; - 不能直接被字符串常量赋值,需要借助 strcpy 或类似函数复制字符串。
大小和长度:
- 字符指针的大小固定4或8字节;
- 字符串的长度由分配的内存大小决定,但实际字符串长度可以通过 strlen 获取。
补充:str[0]等价于*(str + 0),无论str是数组名还是字符指针,都可以通过数组下标的方式来访问和修改内存里的内容(前提是没指向内存中只读区域)
虽然7里的 str 是一个指针,但你可以使用数组下标的方式来访问和修改这块内存中的内容。这是因为在C语言中,str[0] 实际上是 *(str + 0) 的语法糖,它们是完全等价的。
语法糖(Syntactic Sugar) 是编程语言中的一个术语,指的是那些为了让代码更易读、更简洁而引入的语法特性。它并不会增加语言的功能,只是提供了一种更“甜”的写法,让程序员写代码时更舒服。
- arr[i] 是 *(arr + i) 的语法糖。
- for (int i = 0; i < 10; i++) 是对 while 循环的语法糖。
8、不能取地址的常量表达式
int a = &(10+20); //会被报错
在这里,10+20是一个常量表达式。编译器在编译过程中会把它替换为它自己的值,也就是30。30 是即时值,没有自己的内存地址,因此无法被取地址。
编译器在报错中写道:&作为单目运算符,它的对象只能是左值。
9、用 const 声明的常量可以被取地址,但是,它只能由const声明的指针指向,除非强制类型转换来绕过检查,但是会导致未定义行为
const int x = 10;
const int *p = &x; // 合法:p 是 const int * 类型
int *q = &x; // 非法:q 是 int * 类型
因为int * q = &x; 一旦允许,你可能会通过*q改变x的值,这是违反了const的要求的。
10、函数标识符 = 函数名
- 函数名在表达式中会自动转换为指向该函数的指针。
- 函数名:标识符,标识函数;
- 函数指针:变量,存储函数地址;
void foo() {
printf("Hello, world!\n");
}
int main() {
void (*func_ptr)() = foo; // foo 自动转换为函数指针
func_ptr(); // 调用 foo 函数
return 0;
}
11、整型和基本整型
- “整型” 是一个广义的概念,包括 short、int、long、long long、char、unsigned等在内的所有 整数类型;
- int 的中文称呼是“整型” 或 “基本整型”。
12、直接递归是函数体内部调用自己,间接递归通过调用其他调用自己的函数间接调用自己
间接递归的代码:
#include <stdio.h>
// 函数声明
void functionA(int n);
void functionB(int n);
// 间接递归:functionA 调用 functionB,functionB 调用 functionA
void functionA(int n) {
if (n > 0) {
printf("A: %d\n", n);
functionB(n - 1); // 调用 functionB
}
}
void functionB(int n) {
if (n > 0) {
printf("B: %d\n", n);
functionA(n - 1); // 调用 functionA
}
}
int main() {
functionA(5); // 启动间接递归
return 0;
}
对比两者
- 直接递归:代码结构简单直观。
- 间接递归:通过其他函数间接调用自身,代码结构稍复杂,能实现更灵活的递归逻辑。
- 函数的栈帧:
- 创建时间:函数调用时
- 存储内容:局部变量(一次性给所有{}外的auto分配存储单元,初始化按代码顺序来)、参数、返回地址
13、全局变量的定义与声明
- 定义:分配内存
int global_var = 10;
- 只能一次;
- 声明:变量存在哦,变量是xx类型,不要分配存储空间。(类似要吸管、要热饮,不要放年糕~)
extern int global_var;
- 可能有多次声明(但是这是为了支持头文件包含机制);
这里我的问题是:为什么需要多次声明?
如果全局变量定义在文件的末尾,但在前面的代码中需要使用它,就必须通过 extern 提前声明一次。如果全局变量定义在源文件中,在头文件里要用extern声明,其他文件包含头文件。
14、C语言支持两种调用方式
- 传值调用:数值的副本,无法影响实参。
- 传地址调用:地址值的副本,需要指针,通过解引用可以改变函数外定义的变量的值。
引用调用是C++允许的。
全局变量:作用域整个文件 → 无需指针or参数传递,所有函数内部可以直接访问和修改全局变量。
🏆 好啦,恭喜我自己走过这段旅程,我们是冠军!