C语言 基础知识整理

前言

本文整理C语言基础知识,用于开发中日常查阅。

C数据类型

类型 描述
基本类型 它们是算术类型,包括两种类型:整数类型和浮点类型
枚举类型 它们也是算术类型,被用来定义在程序中只能赋予其一定的离散整数值的变量
派生类型 它们包括:指针类型、数组类型、结构类型、共用体类型和函数类型。
void类型 类型说明符 void 表明没有可用的值。

printf

Code格式 描述
%c 字符
%d 带符号整数
%i 带符号整数
%e 科学计数法,使用小写e
%E 科学计数法,使用大写E
%f 浮点数
%g 使用%e或%f中较短的一个
%G 使用%E或%f中较短的一个
%o 八进制
%s 一串字符
%u 无符号整数
%x 无符号十六进制数,用小写字母
%p 一个指针
%n 参数应该是一个指向一个整数的指针指向的是字符数放置的位置
%% 一个%符号

字符指针和字符数组

    char *p = "hello"; // 字符串常量池地址赋值给p,即p指向了字符串常量池的hello
    char *p1 = "hello";
    char c[10] = "hello"; // 是把字符串常量池里的hello strcpy给了c

    printf("%p, %p, %p\n", p, p1, c); // p和p1指向同一个地方
    printf("%d, %d, %d, %d\n", (p == p1), (*p == *p1), (c == p), (*c == *p));
    printf("%c, %c\n", *c, *p);

    c[0] = 'H'; // 可以赋值,在栈空间上
//    p[0] = 'H'; // 会崩溃,在常量池里,不可写
    puts(c);

    char *p2 = malloc(10 * sizeof(char));
    strcpy(p2, "hello");
    p2[0] = 'H';
    puts(p2); // 可以修改,在堆上分配

    printf("===");

    char *p3 = malloc(10 * sizeof(char));
    p3 = "hello"; // 把常量池hello地址赋给p3,造成内存泄露,刚才在堆上分配的10个空间大小没法释放
//    p3[0] = 'H'; // 不能修改,因为是常量池的
    puts(p3); // 可以修改,在堆上分配

    p = "world";
    strcpy(c, "world"); // 重新给c赋值
    puts(p);
    puts(c);

数组指针和指针数组

  • 数组指针(int指针):
    数组指针可以说成是”数组的指针”,指向一个数组的首地址。
    int (*p)[2]: 数组指针,pz指向一个内含2个int类型值的数组
// 传递数组指针,列数为2;
void printArr(int (*p)[2], int row) {
    
    

    // *p指向行,*(*p)指向具体元素
    for (int i = 0; i < row; ++i) {
    
    
        for (int j = 0; j < sizeof(*p) / sizeof(int); ++j) {
    
    
            printf("%d,", *(*(p + i) + j));  // *p指向行,*(*p)指向具体元素
            printf("%d,", p[i][j]);  // 同样可以这么访问
        }
        printf("\n");
    }
}

void test() {
    
    

    int zippo[4][2] = {
    
    {
    
    2, 4},
                       {
    
    6, 8},
                       {
    
    1, 3},
                       {
    
    5, 7}};
    int (*pz)[2]; // 数组指针,pz指向一个内含2个int类型值的数组
    pz = zippo; // 该二维数组首地址就是一个数组指针

    printArr(zippo, 4); 
    printArr(pz, 4);
}

  • 指针数组(int数组)
    指针数组可以说成是”指针的数组”,即这个数组里的每个元素都是指针变量;在32位系统中,指针占四个字节。
    int *p[2]:指针数组,长度为2的int指针变量数组。
    char *a = "1";
    char *b = "2";
    char *arrA[] = {
    
    a, b}; // 字符串数组,用指针数组来表达
    char *arrB[] = {
    
    "1", "2"}; // 字符串数组,用指针数组来表达
    printf("%s,%s\n", arrA[0], arrA[1]);
    printf("%s,%s\n", arrB[0], arrB[1]);

函数指针和指针函数

  • 函数指针(int指针),该指针指向的是一个函数
int (*pfun)(int, int)// pfun是一个指向返回值为int的函数的指针
  • 指针函数(int函数),即返回指针的函数:
    类型名 *函数名(函数参数表列);
int *pfun(int, int); // pfun是一个返回值为整型指针的函数

指针常量和常量指针

    int a = 1;
    int b = 2;

    // 常量指针,指针指向的值不可用改,但指针的指向可以改
    const int *p = &a;
    // *p = b; 错误,指向的值不可用改
    p = &a; // 指针的指向可以改

    // 指针常量,指针的指向可以改,指针指向的值可以改
    int *const p2 = &a;
    *p2 = b; // 指针指向的值可以改
    // p2 = &b; 错误,指针的指向不可用改

    // const修饰指针和常量,都不可用改
    const int *const p3 = a;
    // *p3 = b;
    // p3 = &b;

指针和引用

&取地址操作符,通过该操作符可以获取一个变量的地址值;
取值操作符为*,也叫解引用,通过该操作符可以拿到一个地址对应位置的数据;“解”就是把该地址对应的东西解开,解出来,就像打开一个包裹一样,那就是该变量的值了,所以称为“解引用”。也就是说,解引用是返回内存地址中对应的对象。

指针和引用是一个概念,通常我们说某某变量的引用,即是说该变量的指针。
但是指针和指针变量是两个概念。即一个变量存放另一个变量的地址,那么该变量就叫做指针变量。
指针长度32位下是4个字节,64位下是8个字节。

指针的本质就是间接访问。

  • 指针的使用场景:传递和偏移
    函数调用原理:值传递;实参的值赋给形参(编译原理)

指针偏移

    // 需要区分清楚指针++的操作
    int arr[3] = {
    
    1, 5, 9};
    int *p;
    int j;

    p = arr;
    j = *p++;
//    j = (*p)++;
//    j = *++p;
//    j = ++*p;
//    j = *p++;
    printf("arr[0]=%d, j=%d, *p=%d\n", arr[0], j, *p);

    j = p[0]++;
    printf("arr[0]=%d, j=%d, *p=%d, p[0]=%d", arr[0], j, *p, p[0]);

动态存储方式和静态存储方式

从变量的作用域角度来说可以分为全局变量和局部变量;从变量值存在的时间角度来说可以分为静态存储方式和动态存储方式;
静态存储方式:指在程序运行期间由系统分配固定的存储空间方式;
动态存储方式:是在程序运行期间根据需要进行动态的分配存储空间的方式;

  • 有以下存储类别,根据变量的存储类别可以知道变量的作用域和生存期
    auto:不声明为static的局部变量都是动态存储方式,默认修复符为auto。在函数结束时自动释放这些存储空间。
    static:在静态存储区分配空间,在编译期间进行赋值,只初始化一次。
    extern:借用在别的文件定义的变量,表明这是一个声明不是定义,让编译器去别处查询其定义;
    register:寄存器变量,很少用;

  • tips:
    函数默认是extern的;
    static修饰函数表示在本文件内有效;
    static修改变量表示是静态变量,在本文件内有效;
    静态局部变量:虽然是静态的,但其它函数无法引用它。

运算符

image.png

结构体

结构体对齐;
结构体在函数中传递是值传递,直接拷贝所有内容到栈空间上,所以不会改变调用方的值;需要使用指针传递引用,引用传递就可以避免这种问题。

typedef struct student {
    
    
  int num;
  struct student *pNext; // 此时不能省略student结构体标签
} stu;

typedef和#define

  • typedef允许程序员为现有类型创建别名,可以做到代码即注释;
    通常,typedef定义中用大写字母表示被定义的名称,以提醒用户这个类型名实际上是一个符号缩写。
    用typedef来命名一个结构类型时,可以省略该结构的标签:
    使用typedef时要记住,typedef并没有创建任何新类型,它只是为某个已存在的类型增加了一个方便使用的标签。

  • #define又称宏定义,标识符为所定义的宏名,简称宏。
    其特点是:定义的标识符不占内存,只是一个临时的符号,预编译后这个符号就不存在了。预编译又叫预处理。预编译不是编译,而是编译前的处理。这个操作是在正式编译之前由系统自动完成的。

    • 其实预编译所执行的操作就是简单的“文本”替换。
      对宏定义而言,预编译的时候会将程序中所有出现“标识符”的地方全部用这个“常量”替换,称为“宏替换”或“宏展开”。
      替换完了之后再进行正式的编译。所以说当单击“编译”的时候实际上是执行了两个操作,即先预编译,然后才正式编译。#include<stdio.h>也是这样的,即在预处理的时候先单纯地用头文件stdio.h中所有的“文本”内容替换程序中#include<stdio.h>这一行,然后再进行正式编译。
  • typedef#define的区别
    与#define不同,typedef创建的符号名只受限于类型,不能用于值。
    typedef由编译器解释,不是预处理器。
    在其受限范围内,typedef比#define更灵活。

#define PI 3.1415926 // 可以用于值

typedef int SOCKET; // 给int命个别名SOCKET,之后代码里可以使用SOCKET来表示socket的fd,做到了代码即注释,方便阅读
#define SOCKET int //等同于上句

typedef char* STRING;
STRING a,b; // 等同于 char *a, char *b

#define STRING char*
STRING a,b // 等同于char *a, b; 只有a是指针,文本替换而已;

头文件

#表示这是一条预处理命令。凡是以“#”开头的均为预处理命令。

头文件是一个文件,通常是源代码的形式,由编译器在处理另一个源文件的时候自动包含进来。
头文件中一般放的是同名.c文件中定义的变量、宏、系统全局变量、函数原型的声明,需要让.c外部使用的声明。

  • 头文件作用:
    1. 方便开发:包含一些文件需要的共同的常量、结构、类型定义、函数、变量申明;
    2. 使函数的作用域从函数声明的位置开始,而不是函数定义的位置(实践总结)
    3. 提供接口:对一个软件包来说可以提供一个给外界的接口(例如: stdio.h)。

#include的两种语法

  • #include <file>
    这种形式用于引用系统头文件。它在系统目录的标准列表中搜索名为 file 的文件。在编译源代码时,可以通过 -I 选项把目录前置在该列表前。

  • #include "file"
    这种形式用于引用用户头文件。它在包含当前文件的目录中搜索名为 file 的文件。在编译源代码时,可以通过 -I 选项把目录前置在该列表前。

只引用一次头文件

  • 头文件中添加 #ifndef/#define/#endif 是为了防止该头文件被重复引用。

  • 被重复引用
    是指一个头文件在同一个cpp文件中被include了多次,这种错误常常是由于include嵌套造成的。
    比如:存在a.h文件#include "c.h"而此时b.cpp文件导入了#include “a.h” 和#include "c.h"此时就会造成c.h重复引用。

  • 头文件被重复引用引起的后果:

    1. 有些头文件重复引用只是增加了编译工作的工作量,不会引起太大的问题,仅仅是编译效率低一些,但是对于大工程而言编译效率低下那将是一件多么痛苦的事情。
    2. 有些头文件重复包含,会引起错误,比如在头文件中定义了全局变量(虽然这种方式不被推荐,但确实是C规范允许的)这种会引起重复定义。
  • #ifndef HEADER_FILE意思是"if not define HEADER_FILE" 如果不存在HEADER_FILE

#ifndef HEADER_FILE
#define HEADER_FILE

the entire header file file

#endif

这种结构就是通常所说的包装器 #ifndef。当再次引用头文件时,条件为假,因为 HEADER_FILE 已定义。此时,预处理器会跳过文件的整个内容,编译器会忽略它。

#define  定义一个预处理宏
#undef   取消宏的定义
#if      编译预处理中的条件命令, 相当于C语法中的if语句
#ifdef   判断某个宏是否被定义(#define过), 若已定义, 执行随后的语句
#ifndef  与#ifdef相反, 判断某个宏是否未被定义
#elif    若#if, #ifdef, #ifndef或前面的#elif条件不满足, 则执行#elif之后的语句, 相当于C语法中的else-ifelse    与#if, #ifdef, #ifndef对应, 若这些条件不满足, 则执行#else之后的语句, 相当于C语法中的else
#endif   #if, #ifdef, #ifndef这些条件命令的结束标志.
defined   与#if, #elif配合使用, 判断某个宏是否被定义
ifdef的使用和#if defined()的用法是一样的。
ifndef又和#if !defined()的用法一样。
#pragma  说明编译器信息
#warning 显示编译警告信息
#error   显示编译错误信息

整型常量

/*
 * 符号常量,简单替换,使用时要注意。
 */
#define PI 3 + 2
// #define PI (3 + 2),这样就对了

void test(void) {
    
    
    int i = PI;
    int j = PI;
    printf("i = %d, j = %d\n", i, j);

    int k = PI * PI; // 3 + 2 * 3 + 2 = 11
    printf("k = %d", k);
}

补码

补码就是为了表示负数
补码 = 原码取反 + 1

0000 0000 0000 0000 0000 0000 0000 0101  5
1111 1111 1111 1111 1111 1111 1111 1011 -5  // 取反+1

整型变量

有符号数最高位表示符号;
image.png

void test() {
    
    
    int i = 0x80fb0000; // 首位为符号位
    unsigned int ui = 0x80fb0000;
    short j = 32767; // 0111 1111 1111 1111
    long k = 0;

    printf("%d, %d, %d\n", sizeof(j), sizeof(i), sizeof(k));
    printf("%d, %u\n", i, ui); // 使用%u输出unsiged

    printf("%d\n", j);
    ++j;
    printf("%d", j); // 溢出,使用时防止溢出
}

浮点类型

字符类型

    char c = 'c'; // 1字节
    printf("%c\n", c);

    printf("abc\rd\n"); // \r回到行首,只输出d

    printf("abc\b\b"); // 退格

    char a = '\0'; // 空字符,用于标示字符串结尾,不是空格,打印不出
    printf("%c", a);

猜你喜欢

转载自blog.csdn.net/u014099894/article/details/111842231