在学习前,需要有一定的C语言基础。不必很深入,只需要知道函数,头文件,指针,数组等的概念就可以,但并非0基础笔记。
由于写到后面,不好编辑了,决定分成多篇写,请按编号学习,或者直接看目录先。
说明:该文章来自本人学习时的笔记,使用的编译器是Visual Studio,如有错误,可以在评论区或者私信我纠正,谢谢
13.指针基础
13.1内存模型
计算机最小的寻址单位:Byte字节,现在普遍都是一个字节是一个地址单位,1Byte=1bit
变量的地址:变量常常不止占用一个地址,所以变量的地址是首字节的地址
指针:地址
指针变量:存储地址的变量,往往指针变量也叫指针,一般可以理解是指针还是指针变量,(地址≠存储地址的变量)
13.2指针
int * p:存储int类型地址的指针
int的作用是表示指向对象的类型,也表示对象所占内存的大小,和解释那片内存的空间。
变量的类型是int *
int *p,q; //p的类型是int *,q的类型是int
int *p,*q; //p和q的类型都是int*
当时的条件过于苛刻,存储空间是非常宝贵的。
如今建议一个变量写一行,单独声明,提高可读性。
int *p;
int i=1;
p=&i;
printf("*p=%d p=%d",*p,p);
*p 的 * 是解引用
访问 i 和 *p 的区别:但是实际上这个差距由于缓存的设计可以忽略,不必关注这个
i:直接访问内存空间,逻辑上访问一次
*p:间接访问,先访问p得到i的地址,再通过i的地址访问i,逻辑上访问内存两次
13.3野指针
野指针是表示没有初始化的指针,但是有初始值。如果直接对整数值赋值也是野指针,对嵌入式编程的时候可能会要进行整数赋值。
部分编译器在读取野指针的值的时候,可能会编译不过。对野指针解引用,会引发各种问题
千万不要放任野指针不管!
野指针就是不知道指向那块数据的指针
13.4空指针
不指向任何对象的指针,比如比较常见的,在使用链表的时候就可能需要使用空指针。
13.5指针赋值
指针变量赋值:指的是对指针赋值,把指针指向不同的地址
int i=0;
int *p ;
p = &i //这个是对指针变量的赋值
指针变量指向对象的赋值:对指针变量指向的地址所存的内容赋值,把p指针指向i,通过解引用,修改*p的值,来修改i的值。
int i=0;
int *p;
p=&i;
*p=1;//此时i的值被修改为了1
*p=*q的作用把*p的值,修改为*p的值,指针指向的地址并没有改变,但是指针指向地址所存储的值发生了变化。
13.6指针在函数中的应用
13.6.1指针作为参数
指针作为参数,传递的是地址,在被调函数中修改值,会修改主调函数中变量的值。
void foo (int *a){
a=1;//主调函数调用以后,a的值会变成1
}
因为C语言的特点,C语言只能有一个返回值,但是往往传入参数a可以作为返回值使用
在函数中直接使用a改变的是a的地址,对a解引用,直接对*a赋值才是修改a指向内存的值。
void min_max(const int arr[],int n,int *pmin,int *pmax){
//arr[]作为传入参数,pmin和pmax可以作为传出参数返回值
*pmax=arr[0];
*pmin=arr[0];
for(int i=1;i<n;i++){
if (arr[i]<*pmin){
*pmin=arr[i];
}else if (arr[i]>*pmax){//比最小值小一定比最大值小,优化程序的性能
*pmax=arr[i];
}
}
}
13.6.1指针作为返回值
指针作为返回值的时候,要注意函数的生命周期
int *foo(void) {
int arr[] = {1, 2, 3, 4};
return &arr[1];
}
int main(void) {
int *p = foo();
printf("*p = %d\n", *p);
printf("*p = %d\n", *p);
return 0;
}
两次并没有做任何操作,但是值却不一样。
如下图,在执行完foo以后,返回的是arr[1]的地址,第一次读取arr[1]的时候,foo还没有出栈,*p的值被foo修改为了2,所以第一次读取到的是正确的值,当第一次读取完以后,在第二次的时候,foo出栈了,也就是内存中不存在foo的栈帧了,*p指向的地址,所存储的栈帧发生了变化,故第二次读取到的值为不是2,由于编译器不一样可能读取到不同的值,一些编译器会把未初始化的值修改为0,一些编译器会直接读取指向内存的“脏数据”。
教训:千万不要返回指向当前栈帧的指针
13.7指针常量与常量指针
指针常量指的是指针指向的一个地址,对这个地址的内存,对这个指针来说解引用以后是一个常量,只有读权限,没有写权限。但是可以指向不同的地址
常量指针指的是指针指向一个地址,对这个指针变量来说,他不能修改指向的地址,但是可以修改这个地址的内存。
int i=10,j=20,k=30;
int* p=&i;
*p=10;//*p对i的内存有写权限
const int* q=&i;//指针常量
//*q=10; [Error] assignment of read-only location '* q'
//*q对i就没有写权限,i的内存可以修改,但不能通过p去修改
int* const c = &j;
*c = 10;//常量指针可以修改内存,但是不能修改其指向的地址
//c=&j; [Error] assignment of read-only variable 'c'
常量指针对地址1有写权限,对地址2没有
指针常量对地址3没有写权限,对地址4有写权限
如果两个地方都加常量,则两个地址都没有写权限,const的本质是控制变量的权限
如const int * p = &i;
13.4指针与数组的关系
数组在作为参数的时候,会退化为指针
详细请看这篇文章12.4部分:C语言学习笔记(1)-CSDN博客
指针不是整数,指针+1的值实际上是指针向右偏移1的单位
用指针可以用来处理数组
int arr[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
int sum = 0;
for (int *p = &arr[0]; p < &arr[20]; p++) {
//p不会显示越界,但是当p超过arr的边界的时候,会变成野指针,所以一定要确定p的范围
sum += *p;
printf("*p = %d ;sum = %d\n", *p,sum);
}
在必要的时候,数组可以退化成指向他索引为0元素的指针
int arr[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
int sum = 0;
for (int *p = arr; p < arr+10; p++) {
sum += *p;
printf("*p = %d ;sum = %d\n", *p,sum);
}
指针的运算:
指针不是整数,可以相减,结果是整数,大小为偏移量的大小
但是不能相加、乘、除
指针可以进行比较运算
*可以和++,--相结合使用
区分(--的话类比):
*p++,*(p++):值为*p,副作用为p自增 (这个最常使用)
(*p)++:值为*p,副作用为*p自增
*++p,*(++p):值为*(p+1),副作用为p自增
++*p,++(*p):值为*(p+1),副作用为*p自增
14.字符串
14.1字符串字面值
字面值一定是常量,如“Hello world”,但常量不一定是字面值,如 const int i=1;
字符串的书写:
1.直接一行写完
2.使用跨行符
3.两个字符串直接只有空白字符,编译的时候是相邻的
printf("Hi NNNNNNNNN"
"NNNNNNNNNNNN");
14.2字符串的内存模式
字符串的字面值是不可以修改的!
“ABC”在内存中的储存是ABC\0
printf("%c","ABC"[0]);
'\0'是空字符,在代码中占一个字符
14.3字符串字面值支持的操作
常量数组能支持的操作,字符串都可以支持
总纲:
1.C语言没有字符串类型
2.C语言中的字符串很依赖字符串数组的存在,遇到空字符结算
3.C语言中的字符串是一种逻辑类型
4.在C语言的字符串求长度时间复杂度是O(n),strlen的性能会很差
14.4字符串的语法结构
1.声明字符串变量并赋初始值
两个初始化方式是一样的,str2是str1的初始化方式是一样的,只是使用“语法糖”简写(对语法糖的表示可能不太准确,但就是这个·意思),只是机器帮你解决了str2转str1的步骤。
char str2[]={ 'h','e','l','l','o','\0' };//字符串数组初始化式,不要漏了'\0'
char str2[]="hello";//语法糖方式写
初始化的选择:
初始化字符数组使用str1的方式,初始化字符串使用str2的方式,提高可读性
2.一些细节
char str1[]="hello";//长度为6
char str2[10]="hello";//前面6个初始化为和str1一样,后面4个初始化为'\0'
char str3[5]="hello";//长度为5,但不是字符串,因为,没有以'\0'结尾
str的“hello”是字符串,在栈里面
p指向的“hello”是字面值,在代码段,是无法修改的
char str[] ="hello";//"hello":数组的初始化
char* p = "hello";//"hello":字符串的字面值
14.5字符串的读写
输出p个字符,
printf("%.5s",str);//只输出5个字符
14.5.1scanf读写字符串
%s的匹配规则:
忽略前置空的字符,读取字符填入字符数组,遇到空白字符结束
scanf的缺点:
1.不能存储空字符
2.不会检查数组越界
14.5.2puts与gets
puts等价于printf(“s%\n”,str);//输出字符串的时候会自动换行
gets的原理是读取一行数据存入字符数组,并将换行符替换成空字符,gets也不会检查数组越界
14.5.3fgets(str,n)
和get差不多,但可以读取n个数据,会检查避免数组越界,会存储换行符,并在后面添加空字符。
14.6字符串变量的操作<string.h>
前面先说:掌握两个惯用法就好,在实际使用的时候,直接使用库里面的就好,不要自己去实现!!!
14.6.1 strlen(str)
读取一个字符串的长度,时间复杂度为O(n),不会计算空字符
实现:
方法1:
typedef int size_str;
size_str strlen(char *str){
size_str str_len=0;
while(*str!='\0'){
str_len++;
str++;
}
return str_len;
}
改进:领会字符串的方法
size_str strlen1(char *str) {
const char* p=str;
while(*p){ //遍历字符串惯用法
p++;
}
return p-str;
}
14.6.2 strcpy(str1,str2)
复制字符串,但不会检查越界,返回值为str1
strncpy(str1,str2,n);//strcpy的安全版本,n是表示字符串长度(不包括'\0'),保证不越界
实现strcpy:
char* strcpy(char* str1, char* str2) {
while(*str2){
*str1 = *str2;//复制
str1++;
str2++;
}
*str1 = '\0';//不要忘记了最后补空字符
return str1;
}
改进版本:
char* strcpy(char* str1, char* str2) {
while (*str1 ++ = *str2 ++)
;//复制字符串惯用法,可以复制空字符串
return str1;
}
实现strncpy:
char* strcpy(char* str1, char* str2) {
while (*str1 ++ = *str2 ++)
;
return str1;
}
14.6.3 strcat(str1,str2)
字符串拼接,str1一定要数组,而且str1的长度一定要可以存下拼接后的字符串,否则会越界
strncat(str1,str2,n),可以控制字符串长度,n的值为还可以拼接多少个字符(不包括‘\0’)
strncat实现
char* strcat1(char* str1, char* str2, int count) {
char*p = str1;
while (*str1) {
str1++;
}
while ((count-->0)&&(*str1++ = *str1++))
;
return p;
}
14.6.4 strcmp(str1,str2)
字符串比较,返回结果为str1-str2的值
比较规则:字典序,根据ASCII比较
实现:
int strcmp(const char* str1,const char* str2){
while(*str1&&*str2){
if(*str1 != *str2){
return *str1-*str2;
}
str1++;
str2++;
}
return *str1-*str2;
}
14.7字符串数组
在C语言里面,字符串数组是字符数组的数组,本质上就是字符二维数组,
字符串数组 char c[ ] ={"APPLE","APP","APPL","AP","APPL" }在内存中存储的内容如下
字符指针数组:用指针指向空间,只在末尾有一个‘\0’