C基础

1 如何使用C语言

  1. 利用linux中的gcc编译器编译得到可执行文件。

    2 C的组成

2.1 . 关键字

  • 32个关键字:
    变量类型关键字:int char float double void long short
    存储类型关键字:auto register static extern const
    符号类型关键字:sigend unsigend volatile(仅可特定指针访问)
    条件语句关键字: if else switch case default
    循环关键字:for while do goto continue break return
    自定义数据结构关键字:struct union enum
    其他关键字:sizeof typeof

2.2 . 标识符

  • 自定义的 变量名、函数名、结构体名、枚举名、宏名等。
    -遵循以下规则:
    • 由数字、字母、下划线组成。
    • 首个字符不能由数字开头。
    • 大小写敏感。

      2.3 . 运算符

  • 算术运算符:+ - * / % ++ —
  • 关系运算符:==!= < > <= >=
  • 逻辑运算符:&& ||!
  • 赋值运算符:= += -= *= /= %= &= |= ^= <<= >>=
  • 位运算符:& | ^ ~ << >>
  • 其他运算符:sizeof & & &&, () [] ->. ? : sizeof

- 运算符一般优先级:从高到低,位运算符>算数运算符>关系运算符>逻辑运算符>赋值运算符。

- 运算符其实是一种特殊的函数。

2.4 . 变量

变量(函数等同理)的实现过程: 申明变量 -> 定义变量 -> 赋值 -> 使用变量。其中,申明变量是指声明变量的类型、变量名和变量的存储方式,定义变量是指为变量分配内存空间。变量的申明和定义一般通过编译器缺省的机制完成,也就是只需定义变量即可。

注意:const修饰的变量不能被修改。但是仅仅是不可通过变量名修改,而是需要通过指针修改。

1
2
3
4
5
const int a = 10; // 申明常量变量
int b = 20;
b = a; // 编译错误,常量变量不能被修改
int *p = &a; // 指针变量
*p = 30; // 通过指针修改常量变量

2.5 . 输入输出1(字符)

字符的输入输出可以用printf()和scanf()函数实现。
这两个函数分别要对应输入缓冲区和输出缓冲区。

  • 输入缓冲区:scanf 不会立即从输入源读取数据。相反,它依赖于一个输入缓冲区,该缓冲区存储了之前由用户或其他来源提供的输入数据。当 scanf 被调用时,它首先尝试从这个缓冲区中读取需要的数据。如果输入不符合预期的格式,scanf 可能会留下一些字符在输入缓冲区中,这可能会导致后续的 scanf 调用出现问题。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <stdio.h>
int main() {

int a,c;
char b;
printf("Enter a character: \n");
scanf("%d%c%d", &a,&b,&c);
printf("You entered: %c\n", c);
return 0;
}

// output
// Enter a character:
// 1
// 2
// You entered:

原因是读缓冲未结束,导致将\n也读进来b。

解决方法:

1
2
3
4
5
6
7
8
9
10
11
12
#include <stdio.h>
int main() {

int a,c;
char b;
printf("Enter a character: \n");
scanf("%d", &a);
getchar(); //清空输入缓冲区
scanf("%c%d", &b,&c);
printf("You entered: %c\n", c);
return 0;
}

刷新输入缓冲取的函数为fflush(stdin),但是函数不推荐使用。

  • 输出缓冲区较输入缓冲区相似。

在linux中使用unistd.h中的sleep()函数时若不刷新输出缓冲区,则会导致无法输出。

1
2
3
4
5
6
7
8
9
10
11
12
13

#include<unistd.h>
#include<stdio.h>
int main ()
{
while(1)
{
printf("1");
sleep(1);
}

}


解决办法:
1
2
3
4
5
6
7
8
9
10
11
12
#include<unistd.h>
#include<stdio.h>
int main ()
{
while(1)
{
printf("1");
sleep(1);
fflush(stdout);
}

}

或者在printf()函数中加入\nendl来刷新输出缓冲区。

2.6 . 数据结构1(数组)

  • array[]:数组名,数组元素的个数。
  • array:为指向数组的指针。
  • &array:数组第一个元素的地址。
    1
    int array[10]; //申明一个10个整数的数组

2.6.1 数组越界访问

数组越界访问会导致隐性错误。

1
2
3
4
5
6
7
8
9
10
11
12
#include <stdio.h>
#include <string.h>

void main(){
char str1[] = "hello1";
char str2[] = {'h', 'e', 'l', 'l', 'o','2'};
int len1 = strlen(str1);
int len2 = strlen(str2);
printf("The length of str1 is %d\n", len1);
printf("The length of str2 is %d\n", len2);
printf("str2:%s",str2);
}

上述代码中,str2没有以空字符结尾,导致长度计算错误。
但由于编译器为str1和str2分配了连续的内存空间,因此可以通过数组下标访问到其中的元素。
即输出
1
2
3
4
5

The length of str1 is 6
The length of str2 is 12
str2:hello2hello1

2.7 . 数据结构2 (结构体/联合体/枚举)

  1. 结构体
    1. 注意:结构体名,不是指针而是变量名。
    2. 1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      {
      struct abc
      {
      int i;
      int j;
      };
      struct abc *aaa=NULL;
      printf("%p",aaa);
      struct abc bbb;
      printf("%p",bbb);
      // t.c:2115:14: warning: format ‘%p’ expects argument of type ‘void *’, but argument 2 has type ‘struct abc’ [-Wformat=]
      // printf("%p",bbb);
      // ~^
      }
    3. 注意:结构体结合typedef时,重名还为执行。不可立即使用。
    4. 1
      2
      3
      4
      5
      6
      7
      //错误
      typedef struct loopLinkList
      {
      int data;
      loopLL *prev;
      loopLL *next;
      }loopLL;

    5. 联合体

    </ol>

    2.8 . 循环

    • for 循环:for (初始化; 条件表达式; 迭代表达式) {语句}
    • while 循环:while (条件表达式) {语句}
    • do-while 循环:do {语句} while (条件表达式);
    • goto 语句:goto label;

    2.9 . 条件语句

    • if-else 语句:if (条件表达式) {语句} else {语句}
    • switch-case 语句:switch (表达式) {case 常量表达式1:语句; case 常量表达式2:语句; default:语句;}

    2.10 . 函数

    • 函数的声明:返回类型 函数名(参数类型 参数名, …);
    • 函数的定义:返回类型 函数名(参数类型 参数名, …) {函数体}
    • 函数的调用:函数名(实参, …);
    • 递归函数:函数调用自身。

    2.11 . 指针

    • 指针变量:指针变量存储的是内存地址。

    2.11.1 数组指针与指针数组

    • 数组指针:指向数组的指针。但是数组指针无法将数组的信息
    • 指针数组:数组中每个元素都是一个指针。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    int main() {
    int arr[5] = {1, 2, 3, 4, 5};
    int *p = arr; // 数组指针
    int i;
    for (i = 0; i < 5; i++) {
    printf("%d ", *(p + i)); // 指针数组
    }
    return 0;
    }

    2.11.2 常量指针与指针常量

    • 常量指针:指向常量的指针。
    • 指针常量:指针指向常量。
    1
    2
    3
    4
    5
    6
    7
    int main() {
    int a = 10;
    int *const p = &a; // 指针常量 *const
    int const *q = p; // 常量 const
    *p = 20; // 指针常量
    return 0;
    }

    2.11.3 函数指针

    用于函数重载,由于c中本身不支持函数重载,因此需要通过函数指针来实现。不过需要手动指定函数指针。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22

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

    int sub (int a,int b)
    {
    return a-b;
    }

    int abc(int(*p)(int,int),int a,int b)
    {
    p(a,b);
    }

    int main()
    {
    printf("abc(add,1,2)%d\n",abc(add,1,2));
    printf("abc(sub,1,2)%d\n",abc(sub,1,2));
    }

    2.12 . 字符串处理函数

    2.12.1 常见字符串处理函数

    • strlen():计算字符串的长度。
    • strcmp():比较两个字符串是否相等。
    • strcpy():复制一个字符串到另一个字符串。
    • srecat():连接两个字符串。
    • strstr():查找子串在字符串中的位置(地址)。

    2.12.2 strlen

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    #include <stdio.h>
    #include <string.h>

    void main(){
    char str1[] = "hello1";
    char str2[] = {'h', 'e', 'l', 'l', 'o','2'};
    int len1 = strlen(str1);
    int len2 = strlen(str2);
    printf("The length of str1 is %d\n", len1);
    printf("The length of str2 is %d\n", len2);
    printf("str2:%s",str2);
    }

    结果无法输出,str2没有以空字符结尾,导致长度计算错误。

    2.12.3 字符串输入

    1. scnaf(“%s”,str)

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      #include<stdio.h>
      #include<string.h>

      int main()
      {
      char str[100];
      scanf("%s",str);
      printf("%s",str);
      return 0;
      }

      scanf()遇到空格或回车时,会自动结束输入。

    2. gets()

    长度无法限制,会导致缓冲区溢出。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    #include<stdio.h>
    #include<string.h>

    int main()
    {
    char str[100];
    gets(str);
    printf("%s",str);
    return 0;
    }

    gets()会读入空格,当不会读入’\n’。

    1. fgets()
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    #include<stdio.h>
    #include<string.h>


    int main()
    {
    char str[100];
    fgets(str,100,stdin);//会读入\n
    printf("%s",str);
    return 0;
    }

    fgets()可读入包行空格和最后的’\n’。

    2.12.4 strcmp

    strcmp 的返回值取决于两个字符串之间的关系:

    • 如果 str1 小于 str2(即 str1 在字典顺序上排在 str2 之前),则返回一个小于 0 的负数。
    • 如果 str1 等于 str2,则返回 0。
    • 如果 str1 大于 str2(即 str1 在字典顺序上排在 str2 之后),则返回一个大于 0 的正数。
    • 其返回值是第一个不同的字符的差值。
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      #include<stdio.h>
      #include<string.h>

      int main()
      {
      char str1[] = "hello1";
      char str2[] = {'h', 'e', 'l', 'l', 'o','2'};
      int len1 = strlen(str1);
      int len2 = strlen(str2);
      printf("The length of str1 is %d\n", len1);
      printf("The length of str2 is %d\n", len2);
      printf("str2:%s",str2);
      int cmp = strcmp(str1,str2);
      printf("cmp:%d\n",cmp);
      return 0;
      }

    2.12.5 sprintf

    1
    2
    3
    4
    5
    6
    7
    void main()
    {
    char str[12];
    sprintf(str,"%d%d",1,2);
    printf("%s",str);
    }

    注意下为错误的写法:

    1
    2
    3
    4
    5
    6
    void main()
    {
    char *str;
    sprintf(str,"%d%d",1,2);
    printf("%s",str);
    }

    对于多字节:多字节字符和宽字符:对于多字节字符集或宽字符,应该使用相应的函数如 wcscmp 或 mbscmp。

    2.13 . 预处理器

    2.14 . 多文件操作

    2.14.1 头文件

    两种防止头文件重复包含的方式:

    1. 预编译指令:#ifndef 和 #define。
    1
    2
    3
    4
    5
    6
    #ifndef _MY_HEADER_H_
    #define _MY_HEADER_H_

    // 头文件内容

    #endif // _MY_HEADER_H_
    1. pragma once。

    1
    2
    3
    #pragma once

    // 头文件内容
    1. 多文件编译可使用makefile。对于简单的程序也可直接使用gcc -o main main.c -I. -L. -lmylib

    2. 头文件编写原则:

    • 模块间不会互相依赖时,每个模块定义一个头文件即可,主函数文件包含所有模块的头文件即可(主函数一般不用设置头文件)。
    • 模块间存在依赖(循环调用)时,不引入复杂的调用规则就只有重新设计了。为了避免这种情况模块间的调用可以利用主函数实现。

    2.14.2 多文件资源访问

    • 全局变量和静态变量;可以通过extern关键字声明变量在其他文件中可见。但是极容造成命名冲突。故不推荐使用。
    • 可以使用地址传递的方式,访问其它文件的数据。

    定义文件1 file1.c file1.h

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
     #pragme once



    ### 11. 内存管理

    ```c
    #include <stdlib.h> //包含头文件
    void *malloc(size_t size); //申请内存
    void free(void *ptr); //释放内存
    void *realloc(void *prt,size_t size); //重新分配内存
    void *calloc(void *prt,size_t size); //分配内存并初始化为0
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    int main()
    {
    int len = 5;
    int size = sizeof(char)+len+1;
    char * str = malloc(size);//编译器 可能会将mallco优化为calloc
    free(str);
    str = calloc(sizeof(char),size);//calloc的作用是在分配内存的同时进行初始化。
    printf("p0:%p\n",str);
    printf("p:%p\n",str+len-1);
    printf("*str:%d\n",*(str+len-1));
    strcpy(str,"abcde");
    printf("p:%p\n",str+len-1);
    printf("*(str):%d\n",*(str));
    printf("*str:%d\n",*(str+len-1));
    printf("str:%s\n",str);
    char * newStr = realloc(str,size-1);


    str = NULL;
    len = len -1;
    // printf("p:%p\n",str+len-1);
    // printf("*str:%d\n",*(str+len-1));
    printf("p newStr%p\n",newStr);
    printf("*newStr%d\n",*newStr);
    printf("p:%p\n",newStr+len-1);
    printf("*str:%d\n",*(newStr+len-1));
    printf("p:%p\n",newStr+len);
    printf("*str:%d\n",*(newStr+len));
    free(str);
    }
    • realloc在源内存仍然可以存储数据时不会改变内存地址,
    • 而仅将多余的内存空间标记为可用(不再占用),越界访问仍可读到数据。故应注意引索的设置。
    • 此规则对于free也适用,故在free释放内训后需要将指针指向NULL避免越界访问。

    11.2 内存分区

    • 代码段:存放代码和常量(变量只读)数据。
    • 静态区:存放静态变量。
    • 全局变量区:存放全局变量。
    • 栈:存放函数调用时的临时变量。
    • 堆:存放动态分配的内存。
    • 空指针区:存放空指针。

    12. 异常处理

    注意事项

    9. 可能造成隐形错误(不报错)的情况

    1. 数组越界访问:数组的下标越界访问,会导致程序崩溃或其他不可预知的错误。
    2. 指针越界访问:指针访问的地址越界,会导致程序崩溃或其他不可预知的错误。
    3. 野指针:野指针,是指指针指向的地址不是有效的内存地址,会导致程序崩溃或其他不可预知的错误。
      1
      2
      int *p;//野指针
      printf("%d", *p);//此处p指针指向的地址是无效的,会导致程序崩溃
      可采用定义指针时初始化为NULL(空指针)的方式避免此类错误。
      1
      2
      int *p = NULL;//初始化为NULL
      printf("%d", *p);
      空指针会报错。用以提示修改代码。
    1. 内存泄漏:程序运行过程中,申请了内存,但没有释放,导致系统内存不足,导致程序崩溃或其他不可预知的错误。
    2. 死锁:多个进程因互相等待而陷入僵局,导致程序崩溃或其他不可预知的错误。
    3. 栈溢出:函数调用层次过深,导致栈内存溢出,导致程序崩溃或其他不可预知的错误。
    4. 堆溢出:申请的堆内存过大,导致系统内存不足,导致程序崩溃或其他不可预知的错误。
    5. 格式化字符串漏洞:通过格式化字符串漏洞,可以注入恶意代码,导致程序崩溃或其他不可预知的错误。