鹏哥 C 语言

214 阅读39分钟
  • 以下 a 输出的值是多少
int a = 0x11223344;
char *pc = (char *)&a;
*pc = 0;
printf("%x\n", a); //0x 11223300

image.png

image.png

  • 最大对齐数为 8 字节,计算以下结构体大小: 12

image.png

  • atoi: 将字符串转换为 int

atoi 函数会从字符串的开头开始扫描,跳过任何空白字符,直到遇到第一个非空白字符。然后,它会解析可选的正负号,接着解析连续的数字字符,直到遇到非数字字符为止。最后,将解析到的数字字符转换为整数并返回。

  • 小端字节序:把数据的高字节放到高地址,低字节放到地地址; 大端字节序相反; 以 0x11223344 为例, 高位为11,放到高地址; 低位为 44,放到低地址

image.png

  • 数据在内存中的存储
unsigned int a = 200;
unsigned int b = 100;
unsigned char c = a + b;
//输出 300,44
printf("%d,%d", a +b, c);

image.png

  • strlen 取的是 \0 之前字符的个数,如果没有 \0, 取得的值是随机值

  • 分析以下代码输出结果

void pointer_test() {
    int aa[2][5] = {10,9,8,7,6,5,4,3,2,1};
    //&aa 是二维数组的地址, +1 跳过整个二维数组
    int *ptr1 =(int *)(&aa + 1);
    //aa 是首元素地址,也就是第一行一维数组的地址, +1 跳过第一行的地址,为第二行首元素的地址
    int *ptr2 =(int *)(*(aa + 1));
    //随机值 5 1 6
    printf("%d %d %d %d\n",*ptr1,*ptr2,*(ptr1-1),*(ptr2-1));
}
  • p 中存放的是 hello 的第一个字符 'h' 的 "地址", *p 取出的是 h 这个字符
char *p = "hello";
printf("*p:%c\n",*p); //输出 h
printf("p:%p\n", p);  //输出 h的地址 
printf("p:%s\n",p);   //输出 hello
  • 猜凶手
题目名称:
猜凶手
题目内容:
日本某地发生了一件课荣案,警察通过排查确定杀人凶手必为4个嫌疑犯的一个以下为4个嫌疑犯的供词:
A说:不是我,
B说:是C
C说:是D.
D说:C在胡说
已知3个人说了真话,1个人说的是假话,
现在请根据这些信息,写一个程序来确定到底谁是凶手,

char findKiller() {
    char killer = 'a';
    for (; killer <= 'd'; killer++) {
        if ((killer != 'a') + (killer == 'c') + (killer == 'd') + (killer != 'd') == 3) {
            // printf("凶手是%d",killer);
            return killer;
        }
    }
    return '\0';
}



char killer = findKiller();
if (killer != '\0') {
    printf("凶手是:%c\n",killer);
}else {
    printf("未找到凶手\n");
}


  • 从排序好的二维数组中查找元素

image.png


/**
 * 在二维数组中查找指定元素
 * @param arr 二维数组
 * @param rows 数组的行数
 * @param cols 数组的列数
 * @param key 要查找的元素
 * @return 1 找到元素,0 未找到元素
 */
int findNum(int arr[][3], int rows, int cols, int key) {
    int i = 0;          // 从第一行开始
    int j = cols - 1;   // 从最后一列开始

    // 从右上角开始查找
    while (i < rows && j >= 0) {
        if (arr[i][j] < key) {
            // 当前元素小于 key,向下移动一行
            i++;
        } else if (arr[i][j] > key) {
            // 当前元素大于 key,向左移动一列
            j--;
        } else {
            // 找到目标元素
            return 1;
        }
    }

    // 未找到目标元素
    return 0;
}

/**
 * 测试 findNum 函数
 */
void findNum_test() {
    int arr[][3] = {{1, 2, 3}, {4, 5, 6}, {7, 8, 9}};
    int rows = sizeof(arr) / sizeof(arr[0]); // 计算行数
    int cols = sizeof(arr[0]) / sizeof(arr[0][0]);
              

    // 测试用例 1:查找存在的元素
    int ret1 = findNum(arr, rows, cols, 5);
    printf("Find 5: %d\n", ret1); // 预期输出: 1

    // 测试用例 2:查找不存在的元素
    int ret2 = findNum(arr, rows, cols, 10);
    printf("Find 10: %d\n", ret2); // 预期输出: 0

    // 测试用例 3:查找边界元素
    int ret3 = findNum(arr, rows, cols, 1);
    printf("Find 1: %d\n", ret3); // 预期输出: 1

    int ret4 = findNum(arr, rows, cols, 9);
    printf("Find 9: %d\n", ret4); // 预期输出: 1
}
  • 调整奇数偶数顺序

/**
 * 调整奇数偶数顺序,是所有奇数位于偶数前面
 * 数组中元素为 1,2,3,4,5,6 变成 1,3,5,2,4,6
 */
void adjust_odd_even_num(int *ptr, int len) {
    int left = 0;
    int right = len - 1;
    while (left < right) {
        //从前往后遇到偶数了,要换位置
        while (left < right && ptr[left] % 2 != 0) {
            left++;
        }
        //从后往前遇到奇数了,要换位置
        while (left < right && ptr[right] % 2 == 0) {
            right--;
        }
        int tmp = ptr[left];
        ptr[left] = ptr[right];
        ptr[right] = tmp;
    }
}

void adjust_odd_even_num_test() {
    int arr[] = {1,2,3,4,5,6};
    size_t len = sizeof(arr) / sizeof(arr[0]);
    adjust_odd_even_num(arr,len);
    for (int i = 0; i< len;i++) {
        printf("%d ",arr[i]);
    }
    printf("\n");
}
  • 左旋算法

image.png


/**
 * 方式一:
 * @param str
 * @param k  旋转 k 个字符
 */
void left_round(char *str, int k) {
    size_t len = strlen(str);
    size_t time = k % len;
    printf("len:%zu,k:%d,time:%zu\n",len,k,time);
    int i = 0;
    int j = 0;
    for (i = 0; i < time;i++) {
        //先把第一个元素存起来
        char temp = str[0];
        for (j = 0 ; j < len -1;j++) {
            str[j] = str[j+1];
        }
        //ba第一个元素 temp 的值放到最后一个位置
        str[j] = temp;
    }
}

/**
 *方式二: 
 * str: ABCDEF ,k:3 变成 DEFABC
 * 步骤1: 先取出 DEF 放到临时数组中 tmp 中
 * 步骤2: 取出 ABC 拼接到 tmp 后面
 * 步骤3: 把 tmp 的值赋值给 str
 * @param str
 * @param k
 */
void left_round2(char *str, int k) {
    char tmp[256] = {0};
    size_t len = strlen(str);
    size_t time = k % len;
    // printf("len:%zu,k:%d,time:%zu\n",len,k,time);
    //步骤1: 从第 time 个元素开始拷贝到 tmp 中
    strcpy(tmp,str + time);
    //步骤2:把从 0开始的time 个字符拼接到 tmp 的后面
    strncat(tmp,str,time);
    //步骤3: 把 tmp 的值赋值给 str
    strcpy(str,tmp);
}


方式3:



void reverseStr(char *str, int left, int right) {
    while (left < right) {
        char tmp = str[left];
        str[left] = str[right];
        str[right] = tmp;
        left++;
        right--;
    }
}


int len = strlen(str);
int time = k % len;
reverseStr(str,0,time -1);
reverseStr(str,time,len -1);
reverseStr(str,0,len-1);



    • NDEBUG 必须在 #include <assert.h> 之前定义,才能禁用 assert
#define NDEBUG
#include <assert.h>

void reverse() {
    char *str = NULL;
    assert(str != NULL);
    printf("reverse");
}
  • 字符串逆序
void reverse(char *str) {
    assert(str);
    size_t len = strlen(str);
    char *left = str;
    char *right = str + len - 1;
    while (left < right) {
        char temp = *left;
        *left = *right;
        *right = temp;
        left++;
        right--;
    }
}


// char *str = "hello world"; //非法: 字符串字面量(char* str = "hello world")是只读的,不能修改其内容
char str[] = "hello world"; //合法:字字符数组,存储在可写内存区域(通常是栈或堆)。可以安全地修改其内容。
reverse(str);


char str[10000] = {0};
//gets 可以读取包含空格的字符, scanf 是遇到空格就结束
while (fgets(str,sizeof(str),stdin)) {
    reverse(str);
    printf("%s\n", str);
    printf("请输入要翻转的字符串:");
}

  • int * const p 和 int const *p 的区别
int * const p:  指针 `p` 是常量,不能改变其指向的地址,但可以修改所指向的对象的值。
int const * p:  指针 `p` 不是常量,可以改变其指向的地址,但不能修改所指向的对象的值。



int a = 10;
int b = 20;
int * const p = &a;
*p = 30; // 合法,修改 a 的值为 30
 p = &b;  // 非法,p 是常量指针,不能改变其指向的地址
printf("a:%d\n",a); //输出 30


int const *p2 = &a;
p2 = &b;  //合法,p 可以指向不同的地址,此时 *p2 解引用之后得到的值是b,也就是 20

*p2 = 30; //非法,p 指向的对象是常量,不能修改其值

关键词总结

-   **`int * const p`**:

    -   关键词:**指针是常量**。
    -   特点:指针不能改,对象可以改。

-   **`int const * p`**:

    -   关键词:**指向是常量**。
    -   特点:指针可以改,对象不能改。
  • 局部变量不初始化就是野指针, 在 32 位机器上,int 类型的变量占用 4个字节,指针占用4个字节,可使用的最大内存空间是 2 的 32 次方;64位机器上,int 类型的变量占用 4个字节,指针占用8个字节,可使用的最大内存空间是 2 的 64 次方;

  • 分析以下代码的输出值

int arr[] = {1,2,3,4,5};
short *p = (short *) arr;

for (int i = 0 ; i < 4;i++) {
    //p+i 每次访问的是 2 个字节r
    *(p+i) = 0;
}
for (int i = 0 ; i < 5;i++) {
   printf("%d ",arr[i]); //输出 0 0 3 4 5
}

image.png

image.png

image.png

  • 测试一下 arr 中的元素
int arr4[] = {1,2,(3,4),5}; //实际存的是 1,2,4,5
int size = sizeof(arr4) / sizeof(arr4[0]); 、、4
printf("arr4:%zu,size:%d\n",sizeof arr4,size); //sizeof arr4 为 16
for (int i = 0; i<size;i++) {
    //输出 1,2,4,5
    printf("%d ",arr4[i]);
}
  • 以下字符数组区别
char acx[] = "abcdefg";                      //存的是 a b c d e f g \0, 共 8 个字节
char acy[] = {'a','b','c','d','e','f','g'};  //存的是 a b c d e f g 没有 \0,共 7 个字节
printf("sizeof acx:%zu\n",sizeof acx); //8
printf("sizeof acy:%zu\n",sizeof acy); //7
  • 随着数组下标的增大,地址由低到高

  • 一维数组初始化

int arr[10] = {1,2,3,4,5,6};
int arr[] = {1,2,3,4,5,6};
int arr[10]={0};

  • scanf 的占位符 %s 表示读取一个字符串,遇到空格就结束了

  • 库文件包含

查找头⽂件直接去标准路径下去查找,如果找不到就提⽰编译错误。
这样是不是可以说,对于库⽂件也可以使⽤ “” 的形式包含?
答案是肯定的,可以,但是这样做查找的效率就低些,当然这样也不容易区分是库⽂件还是本地⽂件
了。
  • 条件编译

/**
 * 条件编译
 */
void conditional_compile() {
    //条件编译
#if MAX > 10
    printf("hello\n");
#endif


    //多个分支的条件编译
#if M==0
    printf("000\n");
#elif M==1
    printf("1111\n");
#elif M==2
    printf("2222\n");
#else
    printf("33333\n")
#endif

//检查符号是否被定义,定义了就参考编译,否则不参与编译
#if defined N
    printf("定义了M");
#endif

#if !defined N
    printf("未定义M");
#endif

//检查符号是否未被定义过
#ifndef N
    printf("未定义M");
#endif
}
  • 命令行定义

    许多C 的编译器提供了⼀种能⼒,允许在命令⾏中定义符号。⽤于启动编译过程。

    例如:当我们根据同⼀个源⽂件要编译出⼀个程序的不同版本的时候,这个特性有点⽤处。(假定某

    个程序中声明了⼀个某个⻓度的数组,如果机器内存有限,我们需要⼀个很⼩的数组,但是另外⼀个

    机器内存⼤些,我们需要⼀个数组能够⼤些。)



    int arr[SZ]; // SZ 是未定义
    int i =0;
    for (i = 0; i< SZ;i++) {
        arr[i] = i +1;
    }
    for (i = 0; i< SZ;i++) {
       printf("%d ",arr[i]);
    }


    编译指令:

    gcc test.c -D SZ=10 -o test

    其中 -D 用于定义代码中未定义的符号
    

`-D` 是 GCC 的一个选项,用于在编译时定义一个宏。
`SZ=10` 表示定义一个名为 `SZ` 的宏,并将其值设置为 `10`。
  • undef 移除一个宏

image.png

  • 运算符 ##
## 可以把位于它两边的符号合成⼀个符号,它允许宏定义从分离的⽂本⽚段创建标识符。 ## 被称

为记号粘合

这样的连接必须产⽣⼀个合法的标识符。否则其结果就是未定义的。



#define GENERIC_MAX(type) \
     type type##_max(type x,type y) \
     { \
        return x>y?x:y;\
     }


//定义函数
GENERIC_MAX(int); //生成 int_max
GENERIC_MAX(float); //生成 float_max


int r1 = int_max(1,2);
printf("%d\n",r1);
float r2 = float_max(3.1f,4.5f);
printf("%f\n",r2);


  • 运算符 #
运算符将宏的⼀个参数转换为字符串字⾯量。它仅允许出现在带参数的宏的替换列表中。

#运算符所执⾏的操作可以理解为”字符串化“。

当我们有⼀个变量 int a = 10; 的时候,我们想打印出: the value of a is 10 .

就可以写:

#define PRINT(n) printf("the value of "#n " is %d", n);



#define Print(n,format)  printf("the value of" #n" is " format "\n",n)



int a = 1;
// printf("the value of a is %d\n",a);
Print(a,"%d");

int b = 2;
// printf("the value of b is %d\n",b);
Print(b,"%d");

float f = 5.6f;
// printf("the value of f is %f\n",f);
Print(f,"%f");

  • 宏和函数对比

image.png

image.png

#define Malloc(n,type) (type*)malloc(n*sizeof(type))



int *p2 =  Malloc(10,int);
//等价于 
int *p = (int *)malloc(10 *sizeof(int));

  • 观察编译链接流程
gcc test.c -E -o test.i  //预编译、预处理流程, 预处理命令的处理,注释删除,行号,文件标识符
gcc test.i -S -o test.s  //编译, 词法分析,语法分析, 将 C 语言代码转换成汇编代码
gcc test.s -S -o test.o //汇编,将汇编代码翻译成二进制指令(机器指令)

  • 预处理符号
void preprocessor_symbol() {
    printf("file:%s\n",__FILE__);
    printf("line:%d\n",__LINE__);
    printf("date:%s\n",__DATE__);
    printf("time:%s\n",__TIME__);
}
  • 编译和链接

image.png

  • 总结C/C++中程序内存区域划分

image.png

C/C++程序内存分配的几个区域:
1、栈区(stack):在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。 栈区主要存放运行函数而分配的局部变量、函数参数、返回数据、返回地址等。《函数栈帧的创建和销毁》
2、堆区(heap):一般由程序员分配释放,若程序员不释放,程序结束时可能由OS回收。分配方式类似于链表。
3、数据段(静态区):(static)存放全局变量、静态数据。程序结束后由系统释放。3.
4、代码段:存放函数体(类成员函数和全局函数)的二进制代码。
  • 柔性数组
struct st_type {
    int i;
    int arr[0]; //未知大小的数组, arr 是柔性数组的成员 
};



void test() {
    //结构体大小不包括柔性数组成员大小
    printf("%zu\n",sizeof(struct st_type));

    st_type_t *ps = (struct st_type *) malloc(sizeof(struct st_type) + 20 * sizeof(int));
    if (ps == NULL) {
        perror("malloc failed");
        return;
    }
    ps->n = 100;
    int i = 0;
    for (i = 0; i < 20;i++) {
        ps->arr[i] = i +1;
    }
    //调整 ps 指向空间的大小
    st_type_t * ptr = realloc(ps,sizeof(struct st_type) + 40 * sizeof(int));
    if (ptr == NULL) {
        perror("realloc failed");
        return;
    }
    ps = ptr;
    ptr = NULL;
    for (i = 0; i < 40;i++) {
        //只有前 20 个元素有值,后面的都是随机值
        printf("%d ",ps->arr[i]);
    }
    free(ps);
    ps = NULL;
}


在“结构体中”,最后一个元素是 未知大小的 数组,该元素称为柔性数组的成员.
柔性数组的特点:
结构中的柔性数组成员前面必须至少一个其他成员。
sizeof 返回的这种结构大小不包括柔性数组的内存。
包含柔性数组成员的结构用malloc(函数进行内存的动态分配,并且分配的内存应该大于结构的大小,以适应柔性数组的预期大小。

  • free 之后要记得把指针置为 NULL

  • 常见的动态内存错误

NULL指针的解引用操作
对动态开辟空间的越界访问
对非动态开辟内存使用free释放
使用free释放一块动态开辟内存的一部分
对同一块动态内存多次释放
动态开辟内存忘记释放(内存泄漏)
  • 检查以下代码是否有错误
void GetMmeory(char *p) {
    p = (char *)malloc(100);
}


void test() {
    printf("test start\n");
    char *str = NULL;
    //这里其实是传的 str 的拷贝
    GetMmeory(str);
    printf("str 111:%s\n",str);
    strcpy(str, "hello world");
    printf("str 222:%s\n",str);

以上代码运行会报错,原意是通过 GetMmeory 改变 str 的值,但是由于传的不是地址,而是值的拷贝,所以实际在执行完 GetMmeory 时, str 的值还是为 NULL

修改后的代码如下:


方式 一:
void GetMmeory2(char **p) {
    *p = (char *)malloc(100);
}


void test2() {
    printf("test start\n");
    char *str = NULL;
    GetMmeory2(&str);
    printf("str 111:%s\n",str);
    strcpy(str, "hello world");
    printf("str 222:%s\n",str);
    free(str);
    str = NULL;
}

方式 二:


char *getMemory3() {
    return (char *)malloc(100);
}

printf("test start\n");
char *str = NULL;
//这里其实是传的 str 的拷贝
str = getMemory3();
strcpy(str, "hello world");
printf("str:%s\n",str);
free(str);
str = NULL;

  • free

free函数⽤来释放动态开辟的内存,非动态开辟的内存不能使用 free 释放。 如果参数 ptr 指向的空间不是动态开辟的,那free函数的⾏为是未定义的。 如果参数 ptr 是NULL指针,则函数什么事都不做。 malloc 和 free 成对使用 free的参数必须是内存空间的起始地址

  • malloc
int *p = (int *) malloc(INTMAX_MAX); //模拟分配内存失败
if (p != NULL) {
    printf("内存分配成功,起始地址:%p\n",p);
}else {
    perror("内存分配失败");
    return ;
}
  • 联合体大小计算
联合的⼤⼩⾄少是最⼤成员的⼤⼩。
当最⼤成员⼤⼩不是最⼤对⻬数的整数倍的时候,就要对⻬到最⼤对⻬数的整数倍
  • 位段的内存分配 位段的成员可以是 intsigned int 或者是 char 等类型unsigned int2、位段的空间上是按照需要以4个字节(int )或者1个字节(char )的方式来开辟的。位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使用位段。

  • 结构体实现位段 位段的声明和结构体类似, 但有 2 个不同: 1、位段的成员必须是 int ,unsigned int ,signed int、char,在 c99 中位段的声明也可以是其他类型 2、位段的成员后面有一个冒号和数字

struct A {
    int _a: 2; //2 个字节
    int _b: 5; //5 个字节
    int _c: 10;//10 个字节
    int _d: 30;//30 个字节
};

//输出占用 8 个字节
printf("%zu\n", sizeof(struct A));

  • 结构体传参

推荐传递结构体地址,而不是结构体本身, 原因是:函数传参的时候,参数是需要压栈,会有时间和空间上的系统开销。 如果传递⼀个结构体对象的时候,结构体过⼤,参数压栈的的系统开销⽐较⼤,所以会导致性能的下 降, 如果不想要修改结构体的值,可以在参数上添加 const 关键字

  • 修改结构体对齐数
//vs上默认的对齐数是 8,这里改为 1
#pragma pack(1)
struct S5 {
    char c1;  //1  1  1  分别代表 自身大小、默认对消、对齐数
    int n;    //占用4个字节,对齐数 为 1,从 1 的位置开始存放 4个字节
    char c2;  //占用1个字节,对齐数 为 1,从 5 的位置开始存放 1个字节,
    // 由于结构体的大小为最大对齐数(这里最大对齐数为1)的整数倍,值为 6
};
//恢复对齐数
#pragma pack()
  • 如何优化结构体的内存

让空间小的成员尽量放在一起

image.png

  • 结构体的内存对齐规则
 1. 结构体的第⼀个成员对⻬到和结构体变量起始位置偏移量为0的地址处
    2. 其他成员变量要对⻬到某个数字(对⻬数)的整数倍的地址处。
    对⻬数 = 编译器默认的⼀个对⻬数 与 该成员变量⼤⼩的较⼩值。
      - VS 中默认的值为 8
      - Linux中 gcc 没有默认对⻬数,对⻬数就是成员⾃⾝的⼤⼩
    3. 结构体总⼤⼩为最⼤对⻬数(结构体中每个成员变量都有⼀个对⻬数,所有对⻬数中最⼤的)的
    整数倍。
    4. 如果嵌套了结构体的情况,嵌套的结构体成员对⻬到⾃⼰的成员中最⼤对⻬数的整数倍处,结构
体的整体⼤⼩就是所有最⼤对⻬数(含嵌套结构体中成员的对⻬数)的整数倍。
  • 为什么要做内存对齐 用空间换取时间

image.png

  • 理解以下代码
int a[4] = {1, 2, 3, 4};
int *ptr1 = (int *) (&a + 1);
printf("%p,%x",&ptr1[-1], ptr1[-1]);
int *ptr2 = (int *) ((int) a + 1);
printf("%x", *ptr2);

image.png

image.png

image.png

  • signed char 的范围为 -128 ~127, unsigned char 的范围为 0 ~255,

  •  有符号数据类型,用 %d, 无符号数据类型,用 u%打印

  • 以下分别用 %u 和 %d 输出 a 的值是多少

//100000000 00000000 00000000 10000000 - -128 原码
//111111111 11111111 11111111 01111111 - -128 反码
//111111111 11111111 11111111 10000000 - -128 补码
char a = -128;
//%u 打印无符号的整数,%u的角度,他认为内存中存储的是无符号的整数
//由于 a 是 char 智能放 8 个 bit,因此 a 补码的值是:  10000000
//%u 是有符号整数,因此高位补 1,变成 111111111 11111111 11111111 10000000
//另外由于 %u 是无符号数,因此补码、反码和源码都相同,所以 a 的原码也是  111111111 11111111 11111111 10000000,这个值用
//10 进制表示为 4294967168
printf("%u\n",a);  //输出 4294967168
//如果是 %d打印,则是 -128, 将 111111111 11111111 11111111 10000000 转换成 原码
//为 100000000 00000000 00000000 10000000 ,值为  -128
printf("%d\n",a);  //输出 -128

image.png

  • 以下 a,b,c 的值是多少

关键知识点: 有符号数,高位补 1, 无符号数,高位补 0

//char 是有符号还是无符号,取决于编译器,大部分编译器都是有符号的 signed
//char a 相当于 signed char a
//10000000 00000000 00000000 00000001    //-1的原码
//11111111 11111111 11111111 11111110    //-1的反码
//11111111 11111111 11111111 11111111    //-1的补码
//char a 只占用一个字节 8 位,取得是末尾的  11111111
//%d 打印有符号有符号整型,  a 是char 有符号类型,做整型提升,因此高位全补 1 , 变成  11111111 11111111 11111111 11111111
//打印 a 时打印的是原码:10000000 00000000 00000000 00000001,因此是 -1
char a = -1;
signed char b = -1;

//-1 的补码: 11111111 11111111 11111111 11111111,char a 只占用一个字节 8 位,取得是末尾的  11111111
//c 是无符号,做整型提升时,高位全部 0, 变成00000000 00000000 00000000  11111111, 因此补码值是正数的 255
//正数的补码、反码、原码都相同,因此 %d 输出 c 的值是  255
unsigned char c = -1;
//a:-1, b:-1, c:255
printf("a:%d,b:%d,c:%d\n",a,b,c);
  • 判断大小端

int check_style() {
    //a 为 1,,16 进制表示为: 0x 00 00 00 01, 00为高地址, 01为低地址。
    //如果是大端存储(高位存低地址,低位存高地址),存储的是 00 00 00 01,
    //如果是小端存储(高位存高地址,低位存低地址),存储的是 01 00 00 00
    //因此可以判断第一个字节是否为 1 来判断是大端还是小端存储
    int a = 1;
    if (*(char *)&a == 1) {
        printf("小端");
    }else {
        printf("大端");
    }
    return 0;
}

int check_style2() {
    //a 为 1,,16 进制表示为: 0x 00 00 00 01, 00为高地址, 01为低地址。
    //如果是大端存储(高位存低地址,低位存高地址),存储的是 00 00 00 01,
    //如果是小端存储(高位存高地址,低位存低地址),存储的是 01 00 00 00
    //因此可以判断第一个字节是否为 1 来判断是大端还是小端存储
    int a = 1;
    //返回 1 是小端,返回 0 是大端
    return *(char *) &a;
}


  •  整型在内存中的存储 整数有原码、反码、补码,计算机中存储和计算都是用的补码,原因是 使用补码可以将 符号位和数据域统一处理

大端字节序存储:将一个数据的低位字节内容存放在内存的高地址处,高位字节的内容存放在低地址处 小端字节序存储:将一个数据的低位字节内容存放在内存的低地址处,高位字节的内容存放在高地址处

例如 0x11223344, 如果是按照 11223344 存储,说明是大端存储, 如果是按照 44332211 存储,说明是小端存储

我们常用的 x86 的通常是小端存储,而 Keil C51 则为大端模式,很多 ARM ,DSP 的为小端模式

image.png

  • memcmp 比较指针的前 num 个字符的 ASCII 码值的大小
char str1[] = "abcdefg";
char str2[] = "abcdefh";
size_t size = 0;
if (strlen(str1) > strlen(str2)) {
    size = strlen(str1);
}else {
    size = strlen(str2);
}
int ret = memcmp(str1,str2,size -1);
if (ret > 0) {
    printf("%s 比 %s 大",str1,str2);
}else if (ret < 0) {
    printf("%s 比 %s 小",str1,str2);
}else {
    printf("%s 和 %s 相等",str1,str2);
}
  • memset memset 是用来设置内存的,以字节为单位设置成想要的内容
char str[] = "hello world";
memset(str + 6,'x',5);
//输出 hello xxxxx
printf("str:%s\n",str);

int arr[] = {1,2,3,4,5};
//原意是想把 arr 中前 4个元素的值改为 1, 但实际只会改变第一个元素的值,且是把第一个元素的 4个字节全部改为 1,变成
//0x01010101, 也就是 16843009
memset(arr,1,4);
//结果输出 16843009 2 3 4 5 
print_array(arr,sizeof(arr) / sizeof(arr[0]));
  • C 语言规定,memcpy 只要能实现不重叠的拷贝就行,重叠的拷贝交给 memmove

  • memcpy 使用和模拟实现 从 src 的位置向后拷贝 n 个字节的数据到 dest 指向的内存位置


void *my_memcpy(void *dest, const void *src, size_t num) {
    assert(dest != NULL);
    assert(src != NULL);
    void *ret = dest;
    while (num--) {
        *(char*)dest = *(char*)src;
        dest = (char *)dest + 1;
        src = (char *)src + 1;
    }
    return ret;
}


int src[] = {1, 2, 3, 4, 5, 6,7,8,9,10};
int dest[20] = {0};
my_memcpy(dest, src +2, 16);
print_array(dest, size);

  • memmove 使用和模拟实现
void *my_memmove(void *dest, const void *src, int num) {
    assert(dest != NULL);
    assert(src != NULL);
    //存储目标对象的起始地址
    void *ret = dest;
    //目标位置在源位置之前, 由 前 向 后 拷贝
    if (dest < src) {
        char *d = (char *)dest;          // 目标指针
        const char *s = (const char *)src; // 源指针
        while (num--) {
            *d++ = *s++;  // 逐字节拷贝,并递增指针
        }
    } else {
        // 如果目标地址在源地址之后,从后往前拷贝
        char *d = (char *)dest + num - 1;  // 目标指针指向末尾
        const char *s = (const char *)src + num - 1; // 源指针指向末尾
        while (num--) {
            *d-- = *s--;  // 逐字节拷贝,并递减指针
        }
    }
    return ret;
}



int src[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
//把 1,2,3,4,5 这几个数字 移动到 第 3,4,5,6,7的位置
my_memmove(src+2, src, 20); //输出 1 2 1 2 3 4 5 8 9 10 


my_memmove(src2, src2+2, 20); //输出 3 4 5 6 7 6 7 8 9 10 


  • typedef
typedef unsigned int uint;
typedef int*  pint_t;
//将 parr_t 定义为 typedef int(*)[6] 的别名
typedef int(*parr_t)[6];

void typedef_test() {
    int a = 10;
    int *pa = &a;
    pint_t pb = &a;
    printf("pa:%p,pb:%p\n",pa,pb);

    int arr[6] = {0};
    int (*parr)[6] = &arr; //p 是数组指针
    parr_t parr_t = &arr;
    printf("parr:%p,parr_t:%p\n",parr,parr_t);
}
  • 有趣的代码2
void (*signal(int , void(*)(int)))(int);

image.png

使用 typedef 简化后

typedef void(*pf_t2)(int);
pf_t2 signal(int,pf_t2);
  • 有趣的代码1
(*(void (*)())0)();

//把 0 地址处,强制转换成函数指针类型 void (*)(), 这个函数指针类型的参数和返回值都是空, 之后对函数指针//解引用 (*(void (*)())0), 再调用 (*(void (*)())0) ()

image.png

  • 函数指针变量 变量有地址、数组有地址、函数也有地址
int a = 10;
    int *pa = &a; //整型指针变量

    int arr[5] = {0};
    int (*parr)[5] = &arr;  //函数指针变量

    int(*pf) (int,int) = &add;
    //&函数名 和 函数名都是函数地址,没有区别
    printf("add %p\n",&add); //函数地址
    printf("add %p\n",add);
    printf("add %p\n",pf);
  • 二维数组传参

void print(int (*p)[5], int r, int c) {
    int i = 0;
    for (i = 0; i < r; i++) {
        int j = 0;
        for (j = 0; j < c; j++) {
        //p[i] 是 第 i 行的数组名,数组名又表示首元素地址,所以 p[i]表示的是 & p[i][0]
            printf("%d ", *(*(p + i) + j)); //*(arr + i) == arr[i]
        }
        printf("\n");
    }
}

int main() {
    printf("hello world\n");
    int arr[3][5] = {{1,2,3,4,5},{2,3,4,5,6},{3,4,5,6,7}};
    print(arr, 3, 5);
}

二维数组传参其实传递的是首元素地址,也就是第一行地址, arr + i 代表跳过 i 行.

image.png

  • 二维数组 ⼆维数组其实可以看做是每个元素是⼀维数组的数组,也就是⼆维

数组的每个元素是⼀个⼀维数组。那么⼆维数组的⾸元素就是第⼀⾏,是个⼀维数组。

void printArray(int arr[3][5],int r,int c){
    int i = 0;
    for (i = 0; i < r;i++){
        int j = 0;
        for ( j = 0; j < c; ++j) {
            printf("%d ", arr[i][j]);
        }
        printf("\n");
    }
}

*数组指针使用

void array_pointer2(){
    int arr[10] = {1,2,3,4,5,6,7,8,9,10};
    int sz = sizeof(arr) / sizeof(arr[0]);
    int *ptr  = arr;
    for (int i = 0; i < sz;i++){
//        printf("%d ",*(ptr+i));
        printf("%d ", ptr[i]);
    }
    printf("\n");
}
  • 数组指针变量

指向数组的指针 -> 数组指针变量中存放的是数组的地址

int arr[10] = {1,2,3,4,5};
//p 是数组指针变量, p中存放的是整型数组的地址
int (*ptr) [10] = &arr;  //&arr  取出的是数组的地址


char* ch[5];
//pc 是数组指针变量, p中存放的是字符数组的地址

char (*pc)[5] = &ch;



p先和结合,说明p是⼀个指针变量,然后指针指向的是⼀个⼤⼩为10个整型的数组。所以p是 ⼀个指针,指向⼀个数组,叫 数组指针。 这⾥要注意:[]的优先级要⾼于号的,所以必须加上()来保证p先和*结合。

  • 字符串比较

image.png

  • 字符指针
   char ch = 'w';
    char *pc = &ch;
    *pc = 'a';
  • 字符串指针
//常量字符串,不能修改,这里的赋值是将字符串中的首字符地址赋给 p
    const char *p = "abcdef";
    printf("%c\n", *p);
    //使用 %s 打印字符串的时候只需要提供首字符的地址就行,这里的 p 就代表首字符地址
    printf("%s\n", p);
    //%s 不能使用 *p, 因为这里 *p 是代表首字符,字符输出要用 %c, 不是用 %s
    // printf("%s\n", *p);
    //将收个字符 a 修改为 h,修改失败,因为常量字符串不能修改, 因此 加上 const提醒开发者不要修改
    *p = 'e'; //error
    printf("%s\n", p);
  • 指针数组模拟二维数组
int arr1[5] = {1,2,3,4,5};
    int arr2[5] = {2,3,4,5,6};
    int arr3[5] = {3,4,5,6,7};
    int *arr[] = {arr1,arr2,arr3};
    int sz = sizeof(arr) / sizeof(arr[0]);
    int arr1_size = sizeof(arr1) / sizeof(arr1[0]);
    for (int i = 0; i < sz; i++) {
        // printf("%d ", *arr[i]);
        for (int j = 0; j < arr1_size; j++) {
            printf("%d ", arr[i][j]);
        }
        printf("\n");
    }
  • 指针数组

存放指针的数组

int * arr[]  //存放整型指针的数组
int * arr[]  //存放字符指针的数组

  • 二级指针
 int a = 10;
 int * pa = &a;    //pa 是一级指针,指向(存放)的是 a 的地址
 int ** ppa = &pa; //ppa 是二级指针,指向(存放)的是 一级指针变量(pa)的地址 后面的 * 代表 ppa 是指针, 前面的 int * 代表 ppa 指向的 是 int *
  • 优化冒泡排序
int count = 0;

void bubble_sort(int *arr,int sz) {
    int i = 0;
    for (i = 0; i < sz; i++) {
        //假设已经满足顺序要求
        int flag = 1;
        for (int j = 0; j < sz - i - 1; j++) {
            count++;
            if (arr[j] > arr[j + 1]) {
                flag = 0;
                int tmp = arr[j];
                arr[j] = arr[j + 1];
                arr[j + 1] = tmp;
            }
        }
        //减少排序次数 
        if (flag == 1) {
            break;
        }
    }
}
  • 数组访问
for (i = 0; i < sz; i++) {
        // printf("%d\n",arr[i]);   // 方式1
        // printf("%d\n",*(p+i));   // 方式2
        // printf("%d\n",*(arr+i)); // 方式3
        // printf("%d\n",*(i+arr)); // 方式4
        printf("%d\n",i[arr]);// 方式5
}

  • 数组名是数组首元素地址,但有 2 个例外

1、sizeof (数组名):这里的数组名表示整个数组,计算的是整个数组的大小,单位是字节 2、&数组名: 这里的数组名也表示整个数组,取出的是整个数组的地址 除此之外,任何地方使用数组名,都表示数组的首元素地址

  • 传值调用和传址(地址)调用 以未来函数中只是需要主调函数中的变量值来实现计算,就可以采⽤传值调⽤。如果函数内部要修改 主调函数中的变量的值,就需要传址调⽤。

  • strlen 的模拟实现

size_t my_strlen(const char *s) {
    assert(s != NULL);
    size_t count = 0;
    printf("%p\n", s);
    while (*s) {
        count++;
        s++;
        printf("count:%d,%p\n", count,s);
    }
    return count;
}
  • 关闭 assert 机制
#define NDEBUG
#include <assert.h>

如果已经确认程序没有问 题,不需要再做断⾔,就在 #include <assert.h> 语句的前⾯,定义⼀个宏 NDEBUG

  • 指针赋值为 NULL

NULL 代表地址为 0 的地址,不允许读写

 int *p = NULL;
    
 *p = 20;  //访问地址为 0的指针,会报错
 printf("%d\n", *p);
  • 野指针成因

image.png

image.png

  • const 修饰指针变量(放在 * 号前)

const 修饰指针变量,放在 * 的左边,限制的是指针指向的内容,也就是不能通过指针变量来修改它所指向的内容,例如 不能执行 *p = 20, 但是,指针变量本身是可以改变的:p = &m

void const_pointer_test() {
    int n = 10;
    int m = 100;
    const int *p = &n;
    printf("const_pointer_test p1:%p\n", p);

 
    printf("p1:%p\n", p);
//    *p = 20; //error: 指针变量被 const 修饰,不能通过指针变量来修改它所指向的内容

    printf("n = %d\n", n);

    p = &m; //ok 指针变量本身是可以改变的
    printf("p2:%p\n", p);
}

  • const 修饰指针变量(放在 * 号后)

const 修饰指针变量,放在 * 的右边,限制的是指针本身,指针不能改变它的指向,也就是不能执行 p = &m, 但可以通过指针变量修改它所指向的内容,也就是可以执行 *p = 20

void const_pointer_test() {
    int n = 10;
    int m = 100;
     int * const p = &n;
    printf("const_pointer_test p1:%p\n", p);

    printf("p1:%p\n", p);
    *p = 20; //ok:  可以通过指针变量修改它所指向的内容

    printf("n = %d\n", n);

   // p = &m; //error 指针本身不能改变
    printf("p2:%p\n", p);
}

  • void 类型指针 一般 void*类型的指针是使用在函数参数的部分,用来接收不同类型数据的地址,这样的设计可以实现泛型编程的效果

  • 指针变量的大小

指针变量是用来存放地址的,那么指针变量的大小就得是 4 个字节的空间才可以
  • 为什么可以直接修改变量的值,还需要 通过指针去修改
有些时候不方便直接拿到变量,但是可以拿到变量,这样我们就可以通过变量修改值(类似生活中大佬想找某个人不方便出手,可以把这个人的地址交给其他人,其他人通过地址去找到这个人)
  • 指针定义
int a = 20;
int * pa = &a;

//将pa 指向的地址存放的值为 30, 
*pa = 30;

pa 是变量名字, int * 是变量的类型

其中,每个内存单元,相当于一个学生宿舍,一个字节空间里面能放8个比特位,就好比同学们住的八人间,每个人是一个比特位, 每个内存单元也都有一个编号(这个编号就相当于宿舍房间的门牌号),有了这个内存单元的编号,CPU就可以快速找到一个内存空间, 生活中我们把门牌号也叫地址,在计算机中我们把内存单元的编号也称为地址。C语言中给地址起了新的名字叫:指针。| 所以我们可以理解为: 内存单元的编号 == 地址 == 指针

  • 指针类型的意义

1、解引用的权限,1次可以改变多少个字节


int  a = 20;
int *pa = &a;
*pa = 0;
//输出 0, 因为 pa 是 int 类型的指针,一次可以修改 4 个字节
printf("a:%d\n",a);


int  b = 20;
char *pb = &b;
*pb = 0;
//输出 287453952, 因为 pa 是 char 类型的指针,一次只能修改 1 个字节,因此值为 0x00223344
printf("b:%d\n",b );

2、指针加减整数 指针类型决定了指针向前或向后走一步走多远

  • 变量创建的本质其实是在内存中申请空间
int a = 20;  代表向内存申请 4 个字节的空间,用来存放 20 这个数值,这4个字节,每个都有编号(地址)
变量的名字仅仅是给程序员看的 编译器不看名字 编译器是通过!址找内存单テ的T

  • 指针理解
int a = 10;
int *pa = &a;

pa 是指针变量的名字,存的是 a 的地址
* 表示 pa 是指针, 
int 表示 pa 指向的变量 a 的类型是 int

image.png

  • 指针变量的大小

32 位机器上占 4 个字节, 64 位机器上占 8 个字节

  • 右移操作符

image.png

  • char 的取值范围

image.png

  • 统计某个整数中 位 为 1 的个数

/**
 * unsigned 的原因是兼容负数的情况
 * @param n
 * @return 位 为 1 的个数
 */
int count_biy_ones(unsigned int n) {
    int count = 0;
    while (n) {
        if (n % 2 == 1) {
            count++;
        }
        n /= 2;
    }
    return count;
}
  • 导入静态库方式
#pragma comment(lib, "add.lib")
  • printf 返回的是打印的字符个数
printf("%d\n", printf("%d",printf("%d",43)));

1printf("%d",43), 输出 43, 返回值是 2 
2printf("%d",2), 输出 2, 返回值是 1
3printf("%d",1), 输出 1, 返回值是 1

因此输出 4321.

  • 数组传参

数组在作为函数传递的时候,形参和实参的地址是同一个,因此在函数内部修改数组的值,外部传入的实参的数组的值也会被修改, 形参如果是一维数组,数组的大小可以不写

  • C99中的变⻓数组

image.png

  • int 类型打印

有符号的 int, 使用 %d, 无符号的 int 使用 %u, 如果把一个 有符合的 int,例如 -10, 使用 %u 打印,会出现不正确的情况

int n = -100;
printf("n1:%d\n",n);  //输出 10
printf("n2:%u\n",n);  // 输出 4294967196
  • C语言规定 char 类型默认是否带有正负号,由当前系统决定,这就是说, char 不等同于 signed char,它有可能是 signed char,也有可能是 unsigned char 。这一点与 int 不同,int 就是等同于 signed int 。

  • 数据类型最大最小值范围

printf("有符号 char 最大值:%d,最小值:%d\n", SCHAR_MAX,SCHAR_MIN );
printf("无符号 char 最大值:%d\n", UCHAR_MAX);

printf("有符号 short 最大值:%d,最小值:%d\n", SHRT_MAX, SHRT_MIN);
printf("无符号 short 最大值:%d\n", USHRT_MAX);

printf("有符号 int 最大值:%d,最小值:%d\n", INT_MAX, INT_MIN);
printf("无符号 int 最大值:%d\n", UINT_MAX);

printf("有符号 long 最大值:%l,最小值:%l\n", LONG_MAX, LONG_MIN);
printf("无符号 long 最大值:%ld\n", ULONG_MAX);

printf("有符号 long long 最大值:%lld,最小值:%lld\n", LLONG_MAX, LLONG_MIN);
printf("无符号  long 最大值:%lld\n", ULLONG_MAX);

image.png

  • 变量的存放区域

image.png

  • 运算
int score;
score = (5 / 20) * 100;
//输出 0
printf("score1:%d\n", score);
//输出 25
score = (5 / 20.0) * 100;
printf("score2:%d\n", score);
  • 负数求模的规则

负数求模的规则是,结果的正负号由第一个运算数的正负号决定

printf("%d\n", 11 % -5);  //1
printf("%d\n", -11 % -5);  //-1
printf("%d\n", -11 % 5);   //-1
  • 强转类型

double 类型转换成 int 类型, 会将小数点后的数字全部丢掉,没有四舍五入

int a =(int) 3.14;
  • 占位符(加粗的需要重点关注)

image.png

  • 限定宽度

image.png

  • 总是显示正负号

默认情况下, printf()不对正数显示+号,只对负数显示-号。如果想让正数也输出+号,可以在占位符的 %后面加一个+。

printf("%d\n",12);  //输出 12
printf("%d+\n",12); //输出 +12
printf("%d\n",-12); //输出 -12

  • 限定小数位数

输出小数时,默认是显示 6 位小数,有时希望限定小数的位数,举例来说,希望小数点后面只保留两位,占位符可以写成%.2f。

printf("%f\n",3.14);   //输出 3.140000
printf("%.2f\n",3.14); //输出 3.14
  • 使用 *替代最小宽度和小数位数

最小宽度和小数位数这两个限定值,都可以用 * 代替,通过 printf()的参数传入

printf("%*.*f\n",6,2,0.5);
//等价于
printf("%6.2f",0.5);
  • 输出部分字符串

%s 占位符用采输出字符串,默认是全部输出。如果只想输出开头的部分,可以用%.[m]s 指定输出的长度,其中[m]代表一个数字,表示所要输出的长度

printf("%.7s\n", "hello world"); //输出 hello w

上面示例中,占位符 %.7s 表示只输出字符串"helo word"的前7个字符

  • 使用 scanf 提示警告:

image.png

image.png

  • scanf 的返回值

image.png

  • scanf 占位符

image.png

image.png

  • scanf 赋值忽略符

image.png

image.png

  • 变量初始化

在创建变量的时候,给变量一个初始值,是一种好的编程习惯如果不给变量初始化,有的编译器会报错。局部变量不初始化的时候,他的值是随机的.全局变量如果没有初始化,默认是 0

  • 生成随机数

image.png

第 5 章 数组

  • 10 进制如何转 2 进制

image.png

  • 原码、补码、反码

整数的2进制表⽰⽅法有三种,即原码、反码和补码

有符号整数的三种表⽰⽅法均有符号位和数值位两部分,2进制序列中,最⾼位的1位是被当做符号

位,剩余的都是数值位。

符号位都是⽤0表⽰“正”,⽤1表⽰“负”。

正整数的原、反、补码都相同。

负整数的三种表⽰⽅法各不相同。

原码:直接将数值按照正负数的形式翻译成⼆进制得到的就是原码。

反码:将原码的符号位不变,其他位依次按位取反就可以得到反码。 补码:反码+1就得到补码。

补码得到原码也是可以使⽤:取反,+1的操作。

对于整形来说:数据存放内存中其实存放的是补码。

为什么呢?

在计算机系统中,数值⼀律⽤补码来表⽰和存储。原因在于,使⽤补码,可以将符号位和数值域统⼀

处理;同时,加法和减法也可以统⼀处理(CPU只有加法器)此外,补码与原码相互转换,其运算

过程是相同的,不需要额外的硬件电路。

image.png

  • 整数在内存中存的都是补码 (原码取反再加1) 原因是如果用原码相加会出现,整数加负数的结果不正确的情况,以 1 + (- 1) 为例
000000000 000000000 000000000 000000001  //1 的原码
000000000 000000000 000000000 000000001  //1 的补码


100000000 000000000 000000000 000000001  //-1 的原码
111111111 111111111 111111111 111111110  //-1 的反码
111111111 111111111 111111111 111111111  //-1 的补码

//使用原码存储
000000000 000000000 000000000 000000001  // 1 的原码
100000000 000000000 000000000 000000001  //-1 的原码

100000000 000000000 000000000 000000010  //1 -1 得到的值为  -2(最高位为1 代表负数)


//使用补码存储
000000000 000000000 000000000 000000001  // 1 的补码
111111111 111111111 111111111 111111111  //-1 的补码

000000000 000000000 000000000 000000000  //1 -1 得到的值为 0 符合预期


  • 原码-反码-补码转换

image.png

  • 如何在不引入第 3 个变量的条件下,交换两个变量的值
void swap_test() {
    int a = 10;
    int b = 20;
    printf("Before swap: a = %d, b = %d\n", a, b);
    a = a + b;
    b = a - b;
    a = a - b;
    printf("After swap: a = %d, b = %d\n", a, b);
}

以上方法如果在比较小的数字可以,如果 a 和 b 都比较大, a +b 可能发生溢出风险

  • 交互两个数字的方法

/**
 * 引入一个临时变量,可读性高,性能更好
 */
void swap_test1() {
    int a = 10;
    int b = 20;
    printf("swap_test1 Before swap: a = %d, b = %d\n", a, b);
    int c = a;
    a = b;
    b = c;
    printf("swap_test1 After swap: a = %d, b = %d\n", a, b);
}

/**
 * a 和 b 比较大时,相加有溢出风险
 */
void swap_test2() {
    int a = 10;
    int b = 20;
    printf("swap_test2 Before swap: a = %d, b = %d\n", a, b);
    a = a + b;
    b = a - b;
    a = a - b;
    printf("swap_test2 After swap: a = %d, b = %d\n", a, b);
}

/**
 * 可读性差,性能不如临时变量的方式
 */
void swap_test3() {
        int a = 10;
    int b = 20;
    printf("swap_test3 Before swap: a = %d, b = %d\n", a, b);
    a = a ^ b;
    b = a ^ b;
    a = a ^ b;
    printf("swap_test3 After swap: a = %d, b = %d\n", a, b);
}
  • size_t 是一种数据类型,近似于无符号整型,但容量范围一般大于 int 和 unsigned,使用 size_t 是为了保证 arraysize 变量能够有足够大的容量来储存可能大的数组,但凡不涉及负值范围的表示size取值的,都可以用size_t