C语言学习笔记

2019/12/19 Programming

自己整理的C语言零基础学习笔记,只有一小部分,大概率还会更新。

例1:helloworld
//导入头文件,std表示标准库,io表示输入输出,所以这是一个“标准输入输出库”
//include不限于头文件,可导入任何文件
//include后面接<>时表示导入系统文件,接""表示导入自定义文件
#include <stdio.h>

//程序有且只有一个主函数(main函数)
int main()
{
    //printf是stdio.h里系统提供的函数,表示在标准输出设备上打印字符串
    printf("hello world\n"); //printf输出默认不换行
    return 0;
}
例2:手动编译C文件方法
/*
手动编译C文件:
PS *>gcc -o hello hello.c
生成一个名字为hello的可执行文件
PS *>gcc -c hello.c
生成hello.o文件,可抽取为库文件
*/
例3:system函数
//因为没有用到printf,所以没有导入stdio.h头文件
//system函数是stdlib.h头文件提供的,可以执行命令行(cmd)命令
#include <stdlib.h>

int main()
{
    system("calc"); //system还可以通过绝对路径打开应用程序(exe),但有些程序可能会受到保护
    return 0;
}
例4:C程序编译过程
/*
C程序编译过程:
预编译:宏定义展开,头文件展开,条件编译,删除注释,不检查语法
PS *>gcc -E hello.c -o hello.i
编译:检查语法,编译为汇编文件(参数为大S)
PS *>gcc -S hello.i -o hello.s
汇编:将汇编文件编译为目标文件(二进制文件)
PS *>gcc -c hello.s -o hello.o
链接:C程序依赖于各种库,需要把库链接到可执行程序中
PS *>gcc hello.o -o hello.exe
*/
/*
实际上可以一步到位,比如要得到汇编文件,可以直接
PS *>gcc -S hello.c -o hello.s
*/
例5:C语言关键字
/*
跟数据类型有关的关键字:
char, short, int, long, float, double, void
signed: 有符号数字,默认不写
unsigned: 无符号数字,必须写,且为正数
struct: 结构体
union: 联合体/共生体
enum: 枚举
跟存储有关的关键字(用的不多)
auto: 定义局部变量时声明,一般不写
extern, register
static: 定义静态变量
const: 定义常量
跟控制结构有关的关键字:略
其它:sizeof, typedef, volatile
*/
例6:防止exe运行一闪而过
#include <stdio.h>
#include <stdlib.h>

int main()
{
    printf("hello world!\n");
    system("pause");//防止双击运行exe文件时一闪而过,注意要在return之前,同时有导入stdlib.h头文件
    return 0;
}
例7:常量定义/宏定义
#include <stdio.h>
#define PI 3.14159 //宏定义,注意结尾没有分号,建议这样定义常量

int main() {
    //const float pi = 3.14159; //定义常量,这种定义不安全,但仅限于C,C++里这样是安全的
    int r = 2;
    //float per = 2 * pi * r;
    float per = 2 * PI * r;
    printf("The perimeter is %f\n", per); //%f-->浮点型,%lf-->双精度浮点型
    printf("The perimeter is %.2f\n", per); //此处保留小数会四舍五入,但C++里不会,python里也不会
    return 0;
}
例8:格式化占位符
#include <stdio.h>

int main() {
//    int dec = 10;
//    int oct = 010;
//    int hex = 0x10;
//C语言不能直接输入输出二进制数(?)
    signed int n = -12; //定义有符号整型,默认不写signed
    unsigned int m = 12; //定义无符号整型,如果强制添加符号会溢出范围(原理从二进制看)
    int a = 10;
    printf("%d\n", a); //%d-->十进制整型
    printf("%o\n", a); //%o-->八进制整型
    printf("%x\n", a); //%x-->十六进制整型,字母小写
    printf("%X\n", a); //%X-->十六进制整型,字母大写
    printf("%u\n", a); //&u-->无符号十进制
    short s = 10;
    int i = 20;
    long g = 30;
    long long h = 40; //长长整型,一般不用
    printf("%hd\n", s); //%hd-->短整型
    printf("%d\n", i); //%d-->整型
    printf("%ld\n", g); //%ld-->长整型
    printf("%lld\n", h); //%lld-->长长整型
    return 0;
}
例9:scanf函数(1)
#include <stdio.h>

int main() {
    int a;
    //scanf是stdio.h里的函数,表示标准输入
    //&是一个运算符,表示取地址,&a表示变量a在内存中的地址
    //即把通过scanf获得的数据存放在变量a的地址上,即把数据赋值给a
    //scanf("%d", &a); //不要加换行
    //scanf不安全,因为哪怕输入的数据不符合类型要求也不会报错,CLion建议用strtol
    //但在后面使用scanf("%c", &ch)时没有提示,暂时不明白原理
    scanf_s("%d", &a); //VS2109推荐用scanf_s
    printf("%d\n", a);
    return 0;
}
例10:sizeof关键字
#include <stdio.h>

int main() {
    //sizeof不是一个函数,也不需要导入头文件,它能返回一个数据在内存中所占的空间,单位为字节(BYTE)
    //1BYTE = 8bit
    //sizeof的返回值是一个size_t类型,这是一个别名,本质为一个无符号整型
    unsigned int len = sizeof(int);
    printf("int: %u\n", len);
    //short: 2字节
    //int: 4字节
    //long: Windows下为4字节,32位Linux下为4字节,64位Linux下为8字节
    //long long: 8字节
    //实际上整型在内存中的所占空间与操作系统有关
    return 0;
}
例11:字符型(1)
#include <stdio.h>

int main() {
    char ch = 'a';
    printf("%c\n", ch); //%c-->字符型
    printf("size: %lld\n", sizeof(ch)); //字符型大小为1字节
    //以上为在windows64位操作系统下运行语句
    //在32位下格式字符使用%d而非%lld,原因暂时没搞明白
    return 0;
}
例12:字符型(2)
#include <stdio.h>

int main() {
    char ch;
    printf("Please enter a capital alpha:\n");
    //scanf("%c", &ch);
    scanf_s("%c", &ch, 128); //scanf_s需要两个参数,第二个指定读取位数,防止溢出
    printf("The result: %c\n", ch + 32);
    printf("30%%"); //输出百分号的方式
    return 0;
}
例13:查看数据的内存地址
#include <stdio.h>

int main() {
    float f = 3.14f;
    double d = 3231.14;
    int i = 10;
    printf("int: %p\n", &i); //%p-->变量在内存中的地址编号
    printf("float: %p\n", &f);
    printf("%e\n", d); //%e-->科学计数法
    return 0;
}
例14:数据类型的范围
/*
bit,b,比特,位;
Byte,BYTE,B,字节,1Byte=8bit;
WORD,双字节;
QWORD,四字节;
Kb,千位,1Kb=1024b;
KB,千字节,1KB=1024B;
Mb,兆位,1Mb=1024Kb;
MB,兆字节,1MB=1024KB;

有符号:
char类型取值范围(1字节,8位):-2^7~2^7-1
int类型取值范围(4字节,32位):-2^31~2^31-1
计算机把-0作为范围内的最小值,比如在char类型里为-128,在int类型里为-2147483648
无符号:
char类型:0~2^8-1 (0~255)
int类型:0~2^32-1
*/
例15:其它不常用关键字
#include <stdio.h>

int main() {
    volatile int num;
    //如果一个变量声明了但没有赋值也没有使用,在编译时会优化删掉
    //添加volatile限定词可以防止优化
    register int n;
    //建议型指令,如果寄存器有空闲,则把变量保存在寄存器里,而非内存中
    //建议不使用,因为会造成寄存器滥用
    
    return 0;
}
例16:字符串(1)
#include <stdio.h>

int main() {
    //字符串是内存中一连串的char空间,以'\0'结尾
    //比如,'a'占一个字节,"a"占两个字节,因为有两个字符:'a'和'\0'
    /*
     * 一个"hello world"字符串实际在内存中是这样的:
     * |h|e|l|l|o| |w|o|r|l|d|\0|
     * */
    char * b = "hello\0 world";
    char c[11] = "hello world";
    printf("%s\n", b); //%s-->字符串,在内存中从b开始匹配字符,遇到'\0'结束
    printf("%s\n", c); //本来数组应该是c[12],此处人为地把最后一位'\0'挤掉,会多输出一些奇怪的东西
    return 0;
}
例17:字符串(2)
#include <stdio.h>

int main() {
    int a = 10;
    printf("===%5d===\n", a); //规定宽度,如果原数据的长度大于规定的宽度,则按原数据长度输出
    printf("===%-5d===\n", a); //没有“-”默认右对齐,加“-”左对齐
    printf("===%05d===\n", a); //在左边填充0直到达到规定宽度,不能与“-”搭配,且不能在右边填充
    float b = 3.14159f;
    printf("===%7.2f===\n", b); //7表示整体宽度,.2表示保留小数位数
    printf("===%-7.2f===\n", b); //可以左对齐,但一般不填充0
    return 0;
}
例18:putchar函数
#include <stdio.h>

int main() {
    char a = 'n';
    //putchar是stdio.h里的输出函数,专门用于输出字符
    putchar(a); //输出默认不换行
    putchar('B');
    putchar(97); //其参数默认为int,当然也可以是字符
    putchar('\n');
    return 0;
}
例19:scanf函数(2)
#include <stdio.h>

int main() {
    int a, b;
    scanf("%d,%d", &a, &b);
    //需要输入多个数据时,如果scanf里的格式没有加分隔符,例如"%d%d",则默认以输入时的空格或换行作为分隔符
    //如果加了分隔符,如此处的"%d,%d",则以所加分隔符分隔
    //分隔符不能为'\n',因为scanf以'\n'作为输入的结尾(输入完毕后回车使程序继续运行)
    //此亦强调scanf中不能有'\n'
    /*
     * 此外,scanf("%3d%d", &a, &b)可以对输入的整数进行宽度约束
     * (不知道有啥用)*/
    printf("%d\t%d\n", a, b);
    return 0;
}
例20:getchar函数
#include <stdio.h>

int main() {
    char c;
    //getchar()是stdio.h提供函数
    c = getchar(); //从输入设备获取一个字符,没有参数
    //也可以写在文件最后(return之前)防止exe执行时一闪而过
    putchar(c);
    return 0;
}
例21:算术运算符
#include <stdio.h>

int main() {
    int a = 10, b = 7;
    printf("%d\n", a / b); //整除,结果为整型
    printf("%d\n", a % b); //不能用浮点数取余,但python中可以
    return 0;
}
例22:类型转换
/*
类型转换:
short,char < signed int < unsigned int < long < double
float < double
*/
例23:逻辑运算符
#include <stdio.h>

int main() {
    /*
     * C语言中用数字1表示真,数字0表示假,而非true和false
     * */
    int a = 10, b = 20;
    printf("%d\n", a*2 == ++b);
    printf("%d\n", b);
    return 0;
}
例24:goto跳转结构
#include <stdio.h>

int main() {
    printf("a\n");
    printf("b\n");
    goto FLAG; //无条件跳转,定义了标签后可以随意跳转,但是尽量不要在函数间跳转;也可以造成死循环
    printf("c\n");
    printf("d\n");
    FLAG: //注意这里是冒号
    printf("e\n");
    printf("f\n");
    return 0;
}
例25:数组(1)
#include <stdio.h>

int main() {
    int arr[5] = {1, 2, 3, 4, 5}; //数组的定义方式
    printf("%p\n", arr); //数组名是一个指向数组首地址(首元素的地址)的地址常量
    for (int i = 0; i < 5; ++i) {
        printf("%p\n", &arr[i]);
    }
    printf("size: %lld\n", sizeof(arr)); //单个数据大小×个数(不管是否真的有这么多)
    return 0;
}
例26:数组(2)
#include <stdio.h>
#define SIZE 5

int main() {
    int array[] = {1,2,3,4}; //不预先设定长度,按实际长度开辟空间
    for (int i = 0; i < sizeof(array) / sizeof(array[0]); ++i) {
        printf("%d\n", array[i]);
    }
//    const int j = 5; //这样子貌似不行,难道这就是不安全?
    int a[SIZE] = {}; //数组的大小必须预先设定,且必须为常量(不可改)
    for (int k = 0; k < SIZE; ++k) {
        scanf("%d", &a[k]); //输入可以为“10 20 30 40 50”这种格式,默认以空格和换行分隔
    }
    for (int m = 0; m < SIZE; ++m) {
        printf("%d\n", a[m]);
    }
    return 0;
}
例27:冒泡排序
#include <stdio.h>
#define SIZE 6

int main() {
    int arr[SIZE] = {27, 34, 13, 54, 6, 20};
    //for (int i = SIZE - 1; i > 0; --i) {
    //    for (int j = 0; j < i; ++j) {
    //        if (arr[j] > arr[j + 1]) {
    //            int temp = arr[j];
    //            arr[j] = arr[j + 1];
    //            arr[j + 1] = temp;
    //        }
    //    }
    //}
    for (int i = 0; i < SIZE - 1; ++i) {
        for (int j = 0; j < SIZE - 1 - i; ++j) {
            if (arr[j] > arr[j+1]) {
                int temp = arr[j];
                arr[j] = arr[j+1];
                arr[j+1] = temp;
            }
        }
    }
    for (int k = 0; k < SIZE; ++k) {
        printf("%d ", arr[k]);
    }
    return 0;
}
例28:二维数组
#include <stdio.h>

int main() {
    int d_array[3][4] = { //二维数组定义方法
            {1, 2, 3, 10},
            {4, 5, 6, 20},
            {7, 8, 9, 30}
    };
    for (int i = 0; i < 3; ++i) {
        for (int j = 0; j < 4; ++j) {
            printf("%d ", d_array[i][j]);
        }
    }
    printf("\ntotal size: %lld\n", sizeof(d_array)); //大小=行*列*单个数据大小
    printf("row: %lld\n", sizeof(d_array) / sizeof(d_array[0])); //行数
    printf("column: %lld\n", sizeof(d_array[0]) / sizeof(d_array[0][0])); //列数
    //首地址
    printf("%p\n", d_array);
    printf("%p\n", d_array[0]); //这依然是个数组
    printf("%p\n", &d_array[0][0]); //这是个元素,所以要&
    printf("%p\n", d_array[1]); //第二行首地址,跟第一行差了16位
    return 0;
}
例29:定义字符串
#include <stdio.h>

int main() {
    char a[5] = {'h', 'e', 'l', 'l', 'o'}; //定义字符数组,大小为5
    char * b = "hello"; //定义字符串,大小为6,因为字符串以'\0'结尾
    char c[] = {'h', 'e', 'l', 'l', 'o', '\0'}; //等同于字符串b
    char d[6] = {'h', 'e', 'l', 'l', 'o'}; //等同于字符串b,因为空位用0补齐
    //整型0等同于'\0',但不等同于'0'
    printf("%s\n", a); //这里在数组内找不到'\0',所以按理还会向后输出一下乱码
    printf("%s\n", b); //下面这些则不会,因为都能找到'\0'
    printf("%s\n", c);
    printf("%s\n", d);
    //C语言中没有字符串这种数据类型,一般把以'\0'结尾的字符数组作为字符串
    //所以字符串是一种特殊的字符数组
    return 0;
}
例30:字符串的相关问题
#include <stdio.h>

int main() {
    char s[10];
    scanf("%s", s); //这里没有&,是因为s本身就是一个地址,表示字符数组的首地址
    /*
     * 这里需要注意的问题:
     * 1、s的可获取字符数为9个,因为还要预留一个给'\0';
     * (但是测试的时候好像超了也不报错而且也能获取到多余的字符)
     * 2、如果在输入字符中有空格,则空格之后的字符都不会被获取,因为scanf以空格或换行为结束
     * */
    //在使用scanf_s时,第二个参数一定要与字符串定义长度相同,测试如此
    //此例,scanf_s("%s", s, 10);
    printf("%s\n", s);
    char str[] = {"hello"}; //这其实是一个字符串
    char string[][6] = {"hello", "world"};
    //这是一个字符串数组,但要注意第二维定义的数要比字符串中最长的那个还要长一点
    return 0;
}
例31:获取字符串
#include <stdio.h>

int main() {
    char str[100];
    //gets()是stdio.h提供的输入函数
    gets(str); //从标准输入设备中获取字符串,它允许字符串中含有空格
    //但它依然无法识别输入的字符有多少,所以可能会溢出
    printf("%s\n", str);
    /*scanf("%[^\n]", str); //实际上这样也可以获取到输入的空格,但这样有别的缺陷
     * 里边是一个正则表达式,所以scanf和printf函数实际上是格式化输入输出函数
     * f代表format
     * */
    char string[100];
    //fgets()是stdio.h提供的更安全的输入函数
    //它允许输入中有空格,且在第二个参数规定了接受字符串的最大量,所以不会溢出
    //第三个参数是固定的,是系统规定的输入流常量
    //它会把用户输入的最后一个换行也作为接受的字符,
    //所以在输入字符串不大于第二个参数的情况下,接受的字符串结尾有'\n'
    //如果输入字符串大于规定大小了,自然就不会接收到'\n'了,
    //但无论如何最后一定是'\0'结尾
    fgets(string, sizeof(string), stdin);
    return 0;
}
例32:打印字符串
#include <stdio.h>

int main() {
    char str[] = "hello world";
    //puts()是stdio.h提供的字符串输出函数
    puts(str); //输出默认带换行,只能输出字符串
    puts("hello\0world"); //依然会遇到'\0'停止

    //fputs()是stdio.h提供的字符串输出函数
    fputs(str, stdout); //输出不带换行
    //实际上fgets()和fputs()函数主要用于读写文件,其中f代表file
    return 0;
}
例33:字符串长度
#include <stdio.h>
#include <string.h>

int main() {
    char str[100] = "hello world";
    printf("数组大小:%lld\n", sizeof(str));
    //strlen()是string.h提供的函数
    //返回字符串大小,但不包括'\0'
    printf("字符串大小:%lld\n", strlen(str));
    return 0;
}
例34:获取随机数
#include <stdio.h>
#include <time.h>
#include <stdlib.h>

int main() {
    //time()是time.h提供的函数
    //time_t实际是一个长长整型,time()返回的是一个以秒为单位的数字(1970.1.1至今的秒数)
    time_t timer = time(NULL);
    //srand()设定种子,由相同的种子得到的伪随机数是相同的,大概同python下的seed(0)
    //size_t实际是一个无符号长长整型,这里强制转换数据类型
    //srand里的s大概是seed的意思
    //所以这里如果只是想设置一个种子的话,写srand(0)也是可以的
    srand((size_t)timer);
    int r = rand(); //生成随机数
    printf("%d\n", r);
    return EXIT_SUCCESS; //stdlib.h提供的宏,实际值就是0
}
例35:限定随机数范围
#include <stdio.h>
#include <time.h>
#include <stdlib.h>

int main() {
    srand((size_t)time(NULL));
    for (int i = 0; i < 5; ++i) {
        printf("%d ", rand() % 100); //限定随机数范围在0~99
    }
    printf("\n");
    for (int j = 0; j < 5; ++j) {
        printf("%d ", rand() % 51 + 50); //限定随机数在50~100
        //取余后限定在0~50,加偏移量后限定在50~100
    }
    return EXIT_SUCCESS;
}
例36:随机双色球
#include <stdio.h>
#include <time.h>
#include <stdlib.h>
#define REDS 6 //规定红球个数

int main() {
    //红色球6个,1~32,蓝色球1个,1~16
    srand((size_t)time(NULL));
    int red_balls[REDS] = {0}; //手动初始化为6个0,防止自动初始化成一些乱七八糟的数
    int flag = 0; //设立一个判断红球是否有重复的布尔值
    //开始选红球
    int i = 0;
    while (i < REDS) {
        int red_v = rand() % 32 + 1; //限定范围
        for (int j = 0; j < REDS; ++j) {
            if (red_v == red_balls[j]) {
                flag = 1;
                break;
            }
        }
        if (flag == 0) {
            red_balls[i] = red_v;
            i++;
        }
        flag = 0;
    }
    //选完红球
    //开始选蓝球
    int blue_v = rand() % 16 + 1;
    //选完蓝球
    //打印
    for (int k = 0; k < REDS; ++k) {
        printf("%d ", red_balls[k]);
    }
    printf("\n%d\n", blue_v);
    return EXIT_SUCCESS;
}
例37:函数定义
#include <stdio.h>

int add(int a, int b) {
    return a + b;
}

/**
 * 只要函数没调用,则形参不占存储空间,所以形参不能赋值
 * 但是python的形参是可以附加默认值的
 * @param a 一个数
 * @param b 另一个数
 */
void add_p(int a, int b) {
    int sum_t = a + b;
    printf("%d + %d = %d\n", a, b, sum_t);
}

int main() {
    int s = add(2, 4);
    printf("sum: %d\n", s);
    add_p(2, 5);
    //add_p和add都是函数指针(不加括号的情况)
    //返回值不能返回多个值
    //如果实际返回值类型与函数前规定的返回值类型不符,则强行转换为函数前规定的返回值类型
    //如果不能强制转换,就报错
    return 0;
}
例38:声明函数
#include <stdio.h>

/*
 * 函数使用的三个步骤:函数声明、函数定义、函数调用
 * 如果函数在主调函数(main函数)之前定义,则不需要声明
 * 如果在main之后定义,则需要先在之前声明
 * 声明可以多次
 * 以下是三种声明方式(实际上后两步声明冗余)*/
void add_p(int a, int b);
void add_p(int, int);
extern void add_p(int a, int b);
/*
 * 广义上讲,声明中包含定义,定义是声明的一个特例,但并非所有声明都是定义
 * 比如:
 * int b;
 * 既是一个声明也是一个定义
 * 但
 * extern int b;
 * 就只是一个声明
 * 一般来讲,声明不需要建立存储空间,只要有返回值类型,名称,形参类型、个数和顺序就可以了
 * 但定义需要建立存储空间,需要有完整的函数主体
 * */

int main() {
    int a = 10;
    int b = 20;
    add_p(a, b);
    return 0;
}

void add_p(int a, int b) {
    int sum_t = a + b;
    printf("%d + %d = %d\n", a, b, sum_t);
}
例39:exit函数
#include <stdio.h>
#include <stdlib.h>

void kill_y() {
    printf("You are dead.");
    //exit()是stdlib.h提供的函数,可以直接结束整个程序
    exit(0);
}

int main() {
    kill_y();
    printf("After killed"); //不会打印
    return 0;
}
例40:多文件编译
//jmath.h文件下
#ifndef TESTPROJ_JMATH_H //如果没有定义过这个宏
#define TESTPROJ_JMATH_H //就定义这个宏,防止头文件重复包含(你中有我我中有你)

int add_t(int a, int b); //函数或全局变量声明
int mul_t(int a, int b);

#endif //TESTPROJ_JMATH_H

//jmath.c文件下
int add_t(int a, int b) { //函数的具体实现
    return a + b;
}
int mul_t(int a, int b) {
    return a * b;
}

//main.c文件下
#include <stdio.h>
#include "jmath.h" //导入自定义的文件

int main() {
    int c = add_t(2, 3);
    printf("%d\n", c);
    int d = mul_t(3, 4);
    printf("%d\n", d);
    return 0;
}
//在终端使用gcc时这样编译
//PS *>gcc main.c jmath.c jmath.h -o main.exe
例41:指针的定义和使用
#include <stdio.h>

int main() {
    int a = 10;
    //int*是一种数据类型,存储地址编号,*之前的数据类型取决于要存的地址处是什么数据类型
    //比如也可以是char*什么的
    int* p = &a; //取出a的地址,赋给p
    printf("%p\n", &a);
    printf("%p\n", p);
    //此处的*是取值运算符,所以*p表示的是p这个地址上的值,实际上就是a
    //这是通过指针间接修改变量的值
    int b = 19;
    int c = 20;
    *p = 100;
    printf("%d\n", a);
    printf("%d\n", *p);
    //p的值是0x000000000061FE14
    //64位操作系统,所以有8个字节:00 00 00 00 00 61 FE 14
    //地址都是无符号整型,64位系统给p开辟的空间是8个字节(32位是4个字节)
    printf("%lld\n", sizeof(p));
    return 0;
}
例42:指针注意事项
#include <stdio.h>

int main()
{
	char c = 'a';
	int* p = &c;
	printf("%d\n", c);  //输出97
	printf("%d\n", *p);  //输出-858993567
	//在定义指针时一定要与取址的数据类型对应上
	//此处的错误在于,char类型只占1个字节,而用*p根据地址取值时,因为定义的是int*,int类型占4个字节,所以*p会从第一个地址后连续再取三个,从而输出一些奇怪的东西
	return 0;
}
例43:野指针
#include <stdio.h>

int main()
{
	/* 这是一个野指针
	 * 指针也是变量,是变量就可以赋值,只要不超出范围
	 * 如果我们直接给指针变量指定一个值,这个指针就变成了一个野指针
	 * 程序允许野指针存在,但野指针可能指向一个我们并不知道有什么的区域
	 * 所以在操作野指针指向的内存区域时可能会报错
	 * */
	int* p = 0x00ff722c;
	printf("%d\n", *p);
	return 0;
}
例44:空指针
#include <stdio.h>

int main()
{
	/* 这是一个空指针
	 * 空指针指向地址编号为0的空间
	 * 因为地址0-255的区域为系统专用,不允许访问
	 * 所以任何对空指针的操作(读取、写入)都会报错
	 * 空指针一般用于条件判断,比如对一块地址取址失败后,指针变量会变为0
	 * 因此可以通过判断一个指针是不是空指针来判断取址是否失败
	 * */
	int* p = NULL;
	return 0;
}
例45:万能指针
#include <stdio.h>

int main()
{
	/* 万能指针void*并非真的万能
	 * 它可以接受任意类型的变量的地址
	 * 但是没办法通过void*类型的指针变量进行寻址
	 * 因为寻址前会根据指针类型判断寻址个数,比如char*会寻1个字节,int*会寻4个字节
	 * 但void本身并没有大小,它不能用sizeof取大小,所有void*也无法判断寻址个数
	 * 所以在寻址时需要转换数据类型
	 * */
	int a = 10;
	void* p = &a;
	//*p = 100; 这样是不行的
	*(int*)p = 100; //在已知a是int类型时,可以这样转换
	printf("%d\n", a);
	//虽然sizeof(void)不行,但sizeof(void*)可以,因为所有的指针变量大小都是一样的,
	//在32位下为4个字节,64位下为8个字节
	return 0;
}
例46:const和指针
#include <stdio.h>

int main()
{
	const int a = 10;
	//a = 100; 常量不可修改
	int* p = &a;
	*p = 100; //这样就可以修改了,所以const定义的常量是不安全的
	printf("%d\n", a);
	int b = 20;
	int c = 30;
	const int* q = &b;
	//*q = 200; 此处依然不可改
	q = &c; //此处不报错,修改成功,所以此处实际约束的是*q而非q
	printf("%d\n", *q);
	int* const r = &b;
	//r = &c; 这样不行
	*r = c; //修改成功,所以此处实际约束的是r而非*r
	printf("%d\n", *r);
	//所以const约束就近
	//甚至可以这么写
	const int* const s = &b; //但这样就没办法修改了吗?不是的
	//定义一个二级指针
	int** t = &s;
	//此时s是一个一级指针,t是一个二级指针,s存的是一个内存地址,t存的是存这个地址的内存地址
	**t = c;
	printf("%d\n", *s); //修改成功
	return 0;
}
例47:指针和数组(1)
#include <stdio.h>

int main()
{
	//字符型本身也可以转为整型,所以不报错
	int arr[10] = { 1, 2, 3, 'a', 'b' };
	//数组名是一个指向数组第一项的地址,所以可以理解为一个指针(此处为int*)
	printf("%p\n", arr);
	//指针变量+1时,其地址会偏移相应的指针地址所存数据的大小
	//int* p = 地址
	//p + 1 == 地址 + sizeof(int)
	printf("%d\n", *(arr + 1)); //输出2
	printf("%p\n", arr + 1); //比上一个地址多4
	char ch[5] = { 'a', 'b', 'c', 'd' };
	printf("%p\n", ch);
	printf("%p\n", ch + 1); //比上一个地址多1
	printf("%c\n", *(ch + 1)); //输出'b'
	return 0;
}
例48:指针和数组(2)
#include <stdio.h>

int main()
{
	int arr[10] = { 1, 2, 3, 'a', 'b' };
	int* p = arr;
	for (int i = 0; i < 5; i++)
	{
		printf("%d\n", arr[i]);
		printf("%d\n", *(p + i)); //同上,所以i其实就是偏移量
	}
	for (int j = 0; j < 5; j++)
	{
		printf("%d\n", *p++); //先寻址,再自增
	}
	int b = p - arr; //arr是常量不会变,因为数组大小定义为10,所以p指向了数组第六个值
	printf("%d\n", b); //输出5,指针变量相减,结果是偏移量,所有指针类型相减都是int
	return 0;
}
例49:指针和数组(3)
#include <stdio.h>

void func(int array[])
{
	printf("%d\n", sizeof(array));
}

int main()
{
	int arr[10] = { 1, 2, 3, 'a', 'b' };
	//p和arr的不同之处
	int* p = arr;
	printf("%d\n", sizeof(p)); //变量,输出4
	printf("%d\n", sizeof(arr)); //常量,输出40,因为数组大小定义为10
	func(arr); //而此处输出为4,因为数组名在作为函数参数传递的时候退化为指针了
	//因此对于func函数,定义参数int* array效果是等同的,
	//在函数内部调用数组元素实际上也是使用了指针偏移量,即p[1]等同于arr[1]
	return 0;
}
例50:字符串复制(1)
#include <stdio.h>

/**
 * 虽然传递的实参是字符数组,但设置的形参为指针,
 * 因为都一样,数组会退化为指针,而且用指针操作数组与用数组名操作等同
 * while (src[i] != '\0')等同于while (src[i])
 * 最后一步是要在字符串最后添加一个'\0',因为字符串都要以'\0'结尾
 * '\0'等同于0
 * */
void str_cp(char* tar, char* src)
{
	int i = 0;
	while (src[i] != '\0')
	{
		tar[i] = src[i];
		i++;
	}
	tar[i] = '\0';
}

int main()
{
	char s[] = "Hello World";
	char t[20];
	str_cp(t, s);
	printf("%s\n", t);
	return 0;
}
例51:字符串复制(2)
/**
 * 此处两个函数与上例的函数等同
 * 此处两个函数都是用指针偏移量操作数组元素
 * 此处偏移量都是sizeof(char)
 * */
void str_cp2(char* tar, char* src)
{
	int i = 0;
	while (*(src + i))
	{
		*(tar + i) = *(src + i);
		i++;
	}
	*(tar + i) = 0;
}

void str_cp3(char* tar, char* src)
{
	while (*src)
	{
		*tar = *src;
		tar++;
		src++;
	}
	*tar = 0;
}

/**
 * 分步如下:
 * 取值,*tar,*src
 * 赋值,*tar=*src
 * 判断,*tar是否为0
 * 自增,tar++,src++
 * 所以此处是先赋值后判断,最后的0已经赋上,不需要再独自添加
 * */
void str_cp4(char* tar, char* src)
{
	while (*tar++ = *src++);
}
例52:指针运算
#include <stdio.h>

int main()
{
	int arr[] = { 1,2,3,4,5,6 };
	int* p;
	char* q;
	p = &arr[3];
	q = &arr[3];
	printf("%p\n", p);
	printf("%p\n", q);
	for (int i = 0; i < 3; i++)
	{
		p--;
		q--;
	}
	printf("%p\n", p); //减少了12
	printf("%p\n", q); //减少了3,因为指针计算时偏移量跟定义的类型有关,但赋值时无所谓,因为都是一个地址
	return 0;
    //除此之外,指针之间可以相减(见例48),但不能相加,乘除也不行
    //指针可以加减偏移量,但乘除取余都不行
    //可以判断大小,可以逻辑与或运算
}
例53:指针和数组(4)
#include <stdio.h>

int main()
{
	int arr[] = { 1,2,3,4,5,6 };
	int* p;
	p = &arr[3];
	printf("%d\n", p[-2]);
	//arr[-2]是不允许的,数组索引中不能有负值,此处跟python不同
	//但使用指针变量操作数组时就可以有负值了,即负偏移量
	printf("%d\n", p[-5]); //但是这样就超出了范围,不报错但没意义
	return 0;
}

Search

    Table of Contents