012-指针(下)-C语言笔记

/ 0

指针与指针之间的运算

减法运算:

如果有两个指针都分别指向同一个数组中的某个元素,我们可以通过指针减法得到这两个元素之间相差多少个元素。注意指针与指针的减法运算,只有运用在数组中才有意义,并且指针之间只有减法没有加法。

#include <stdio.h>

int main() {
    int arr[5] = {10,20,30,40,50};
    int *p1 = arr;
    int *p2 = &arr[4];
    long num = p2 - p1;//返回long类型
//  long num1 = p2 + p1; //这样是错误的。。。
    printf("num = %ld\n",num);//结果为4,代表这两个指针指向的元素之间相差4个元素
    return 0;
}

关系运算:

可以判断两个指针谁在高字节,谁在低字节,或者是否指向同一个变量等等。

#include <stdio.h>

int main() {
    int arr[5] = {10,20,30,40,50};
    int *p1 = arr;
    int *p2 = &arr[4];
    int result = p1 > p2;
    int result1 = p1 < p2;
    int result2 = p1 == p2;
    int result3 = p1 != p2;
    //.......
    //打印结果,1为真,0为假
    printf("result = %d\nresult1 = %d\nresult2 = %d\nresult3 = %d\n",result,result1,result2,result3);
    return 0;
}

指针与字符串

内存分区

为了便于系统管理,内存之中主要分为5个区域,存储在不同区域的数据我们系统的管理方式是不一样的。

1.栈空间:所有的局部变量存储在栈空间之中。

2.堆空间:可以由程序员手动申请字节空间来使用。

3.BSS段:存储未被初始化的全局变量、静态变量。全局变量虽然有默认值,但是在初始化之前,这个变量是存储在BSS段的。

4.数据段(常量区):存储已被初始化的全局变量、静态变量、常量数据、字符串常量数据。

5.代码段(代码区):执行一个程序,会将这个程序加载到内存,一个程序中其实就是代码,这个程序的代码存储在代码段区域。

//存储方式
char chs[] = "jack";//字符串数据是以字符数组的形式存储在字符数组之中,将字符串的每一个字符存储在字符数组之中,并以'\0'作为结束符。
char *str = "jack";//字符串数据以字符指针的形式存储,直接给一个字符指针初始化一个字符串数据。

//当他们都是局部变量的时候
char chs[] = "jack";//会在栈空间之中创建一个长度为5的字符数组,将字符串的每一个数据存储到该元素中,并自动追加'\0'。
char *str = "jack";//str指针是一个局部变量,所以这个指针声明在栈空间中,"jack"这个字符串是以字符数组的形式存储在数据段之中。str指针存储的是数据段之中字符数组的地址。

//当他们都是全局变量的时候
char chs[] = "jack";//这个时候chs字符数组是创建在数据段之中的
char *str = "java";//str指针存储在数据段,"jack"字符串也是存储在数据段

注意:

  1. 以字符数组存储的字符串,可以通过下标或者指针去修改存储在字符数组之中的每一个元素的值。
  2. 以字符指针的形式存储的字符串,是存储在数据段的,存储在数据段的数据具有恒定性,一旦创建就无法修改。
#include <stdio.h>

int main(){
    char ch[] = "jack";
    char *pp = &ch;//声明一个指针指向字符数组
    *pp = 'a';//这样是允许的
//    ch = "zhou";//这样是不允许的,ch是数组地址,是常量无法修改的

    char *p = "jack";
//    *p = 'a';//这样是不允许的
    printf("p = %p\n",p);//第一个地址、第二个地址相同

    p = "jack";//这里他不会重新创建,因为常量区已经有了,直接返回他的地址给p。地址不会改变
    printf("p = %p\n",p);//第二个地址、第一个地址相同

    p = "zhou";//这里是在数据段重新创建一个新的"zhou"字符串,让指针p指向它。这里只是修改指针指向,不是修改字符串数据
    printf("p = %p\n",p);//第三个地址和第一、第二个地址不同

    return 0;
}

指针与中括号

所有指针名后面都可以写一个中括号,其中写一个整数。

指针名[n] == *(指针名 + n)

#include <stdio.h>
#include <string.h>

int main(){

    char *str = "fafafdfyurfadfaafafgdg";
    int count = 0;
    unsigned long length = strlen(str);

    //第一种写法,*(str + i)
    for (int i = 0; i < length; i++) {
        if (*(str + i) == 'a') {
            count++;
        }
    }
    printf("a一个出现了%d次\n",count);

    count = 0;//重置次数
    //第二种写法,str[i]
    for (int i = 0; i < length; i++) {
        if (str[i] == 'a') {
            count++;
        }
    }
    printf("a一个出现了%d次\n",count);

    return 0;
}

字符串数组

使用二维字符数组来存储多个字符串数据,每个字符串的长度必须限制并且不能超过限制长度,灵活性很差。

#include <stdio.h>

int main(){
    char name[3][20] = {"zhou","jian","feng"};//灵活性差
    return 0;
}

使用字符指针数组来保存多个字符串数据,每一个字符串数据是以字符数组的形式存储在数据段的,字符指针数组的元素存储的是字符串在数据段的字符数组的地址。所以不限制字符串数据的长度,灵活性很好。

#include <stdio.h>

int main(){
    char *name[] = {"zhou","jian","feng","fjalfjlajflajfladjflafj"};//灵活性好
    return 0;
}

案例:对字字符串进行各种排序

#include <stdio.h>
#include <string.h>

int main(){
    //随便整一堆字符串
    char *names[] = {
        "hexiaoping",
        "zhoujinafeng",
        "jack",
        "rose",
        "canglaoshi",
        "laowang",
        "tongzhuo"
    };
    //计算字符指针数组长度
    int length = sizeof(names) / sizeof(char *);

    //用冒泡排序来按照字符串长度来排序
    for (int i = 0; i < length - 1; i++) {
        for (int j = 0; j < length - 1 - i; j++) {
            //分别计算字符串的长度
            int len = sizeof(names[i]);
            unsigned long len1 = strlen(names[j]);
            unsigned long  len2 = strlen(names[j+1]);
            if (len1 > len2) {
                char *temp = names[j+1];
                names[j+1] = names[j];
                names[j] = temp;
            }
            printf("%d ",len);
        }
    }

    //用选择排序来按照字符顺序来排序
    for (int i = 0; i < length - 1; i++) {
        for (int j = i+1; j < length; j++) {
            //比较字符串的大小
            int res = strcmp(names[i], names[j]);
            if (res > 0) {
                char *temp = names[i];
                names[i] = names[j];
                names[j] = temp;
            }
        }
    }

    //遍历字符串,输出结果
    for (int i = 0; i < length; i++) {
        printf("%s\n",names[i]);
    }

    return 0;
}

字符串函数补充

fputs()函数

作用:可以将字符串输出到指定的流之中。可以输出到标准输出流(控制台),也可以输出到文件中。

语法:fputs(字符串,输出流);

关于输出流:stdout 代表标准输出流,也就是我们的控制台。

fopen()函数可以返回指定的文件的指针。

"w" write 写入数据到这个文件,如果文件不存在则创建再写入,如果已经存在则替换文件。

"r" read 从这个文件之中读取数据

"a" apped 追加,在文件末尾追加数据

fgets()函数

作用:从指定的流之中读取字符串数据。这个流可以是标准输入流(控制台),也可以是文件流。

语法:fgets(用于存储读取出来的字符串的字符数组名,最多读取多少长度的字符串,指定流);

当指定流是标准输入流stdin的时候,第二个参数要和数组的长度保持一致。假如第二个参数为n,如果用户输入的字符串长度大于等于了n,只会存储n-1个字符。如果用户输入的字符串长度等于了n- 1,刚好完美。如果用户输入的字符串长度小于了n-1,那就会多接收个'\n'。

案例:从控制台输入字符串,并存储到指定文件

#include <stdio.h>
#include <string.h>

int main(){

    //打开文件
    FILE *fp = fopen("fputs.txt", "w+");

    char str[100];
    if (fp != NULL) {
        printf("请输入字符串存储到文件中\n");

        //从控制台读取99个以内的字符数据并存储到字符数组str中
        fgets(str, 100, stdin);
        unsigned long len = strlen(str);
        if (str[len-1] == '\n') {
            str[len-1] = '\0';
        }

        //从字符数组str从输出字符存储到文件中
        fputs(str, fp);
    }
    fclose(fp);//关闭文件

    return 0;
}

const关键字

在声明变量的同时,用const关键字来修饰这个变量,被const修饰的变量,在某种程度上,这个变量具备不可更改性。像这样的具备不可更改性,叫常量。

作用:让变量具备不可更改性。

//const修饰基本数据类型(int double float char)这些基本数据类型具备不可更改性。
int const num = 10;//让num变量不能被重新赋值。
const int num = 10;//效果同上

//const修饰数组
const int arr[3] = {10,20,30};//arr数组中元素的值不能更改。
int const arr[3] = {10,20,30};//效果同上

//const修饰指针一
int num = 13;
const int *p = &num;//无法通过p指针修改指向num变量的值,但p指针变量的值可以改
int num1 = 10;
p = &num1;//可以
//*p = 10;//不可以

//const修饰指针二
int num = 13;
int const *p = &num;//和一效果相同
int num1 = 10;
p = &num1;//可以
//*p = 10;//不可以

//const修饰指针三
int num = 13;
int * const p = &num;//无法修改p指针变量的值,但可以通过p指针修改指向num变量的值
*p = 100;//可以
//int num1 = 10;
//p = &num1;//不可以

//const修饰指针四
int num = 10;
int const * const p = &num;//都不能改
//*p = 100;//不可以
//int num1 = 10;
//p = &num1;//不可以

常见应用场景

  1. 如果在程序运行期间,有1个数据是不会发生变化的。但在很多地方都有使用,就可以用const修饰。
  2. 当指针作为函数参数的时候,可以在指针参数前加const修饰。就说明这个函数不能修改调用者变量的值。

内存管理

堆空间的特点:

  1. 允许程序员在堆空间中申请任意个字节数的空间来使用
  2. 申请在堆空间之中的字节空间,除非程序员主动释放或者程序结束,否则申请的空间是不会释放的。

申请字节空间的相关函数都在 stdlib.h 系统头文件之中

malloc()函数

使用malloc函数在堆空间申请任意个字节数的空间,是从 低地址向高地址 分配一块连续的字节空间。返回值是申请的那块连续空间的低地址的第一个字节的地址。语法上任意指针类型的变量都能接收这个函数的返回值。为了避免浪费内存空间,我们要根据自己的实际情况声明指针接收。malloc申请是有可能失败的,如果申请失败返回的值是NULL。

#include <stdio.h>
#include <stdlib.h>

int main(){
    int *num = malloc(4);
    if(num != NULL){
        *num = 200;//先判断是否申请成功,然后再操作
    }

    free(num);//当空间不在使用的时候,要手动释放空间
    return 0;
}

calloc()函数

在堆中申请指定字节的空间来使用,其用法和malloc使用一样。唯一不同的是,malloc申请的空间有可能有垃圾值,calloc申请空间时并清空申请到的空间,我们一般也是用calloc来申请空间。

语法:calloc(块数,每块的字节空间);

#include <stdio.h>
#include <stdlib.h>

int main(){

    int *num = calloc(2, sizeof(int));
    //先判断是否申请成功,然后再操作
    if(num != NULL){
        *num = 200;
        *(num + 1) = 300;
    }

    free(num);//当空间不在使用的时候,要手动释放空间
    return 0;
}

realloc()函数

realloc是对已经申请的字节空间增大容量,如果后面空闲空间足够,则直接扩容。如果不够,则找一块新的连续空间,并把原来空间的值赋值过来,原来的空间将自动释放,并返回新的空间的低字节地址。

#include <stdio.h>
#include <stdlib.h>

int main(){

    int *num = calloc(2, sizeof(int));
    //先判断是否申请成功,然后再操作
    if(num != NULL){
        *num = 200;
        *(num + 1) = 300;
    }
    int *num1 = realloc(num, 1600);//将num扩容至1600字节,并返回第一个字节的地址
    printf("num = %p  num1 = %p\n",num,num1);//两个地址不同,则表示已经换到新空间

    free(num1);//当空间不在使用的时候,要手动释放空间
    return 0;
}

free()函数

释放不再使用的手动申请的堆空间,所谓释放是这块空间可以分配给其他人,但里面的值并没有清空。

语法:free(需要释放空间的指针名);