C语言细枝末节

sizeof的返回值最好用zu来打印

#include <stdio.h>

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

    printf("%zu\n", sizeof(char));
    printf("%zu\n", sizeof(short));
    printf("%zu\n", sizeof(int));
    printf("%zu\n", sizeof(long));
    printf("%zu\n", sizeof(long long));
    printf("%zu\n", sizeof(float));
    printf("%zu\n", sizeof(double));

    printf("%zu\n", sizeof(arr));

    short a = 10;
    printf("%zu\n", sizeof a);

    return 0;
}

   sizeof 是一个运算符,用于计算数据类型或变量在内存中占据的字节数。sizeof 返回的是一个 size_t 类型的值,size_t 是一种无符号整数类型,通常用于表示大小或长度。在标准库 <stddef.h> 中定义了 size_t

          在实际使用中,sizeof 运算符经常用于计算数组的长度、动态分配内存时确定分配的空间大小、以及确保不越界访问数组的元素等。

   sizeof(int) 将返回 int 类型在内存中占据的字节数,sizeof(arr) 将返回整型数组 arr 在内存中占据的字节数。打印时使用 %zu 格式说明符来打印 size_t 类型的值。

sizeof(long) >= sizeof(int)

        在一些系统上,sizeof(long) 和 sizeof(int) 的大小可能相同,通常都是4字节。但在其他系统上,它们的大小可能有所不同。在C语言标准中,long 和 int 的大小是由编译器和系统决定的,因此在不同的系统上可能会有所不同。

        一般来说,long 的大小至少和 int 一样大,甚至更大。在一些系统上,long 可能是4字节,而 int 也是4字节,所以它们的大小相同。但在其他系统上,long 可能是8字节,而 int 是4字节。

 作用域

        作用域(scope)用于描述程序中标识符(如变量、函数等)可见的区域。作用域规定了标识符在代码中的可访问范围,有效地管理了变量和函数的命名冲突。

        局部作用域(Local Scope):局部作用域中声明的变量或函数只能在其所在的代码块内访问,作用域从声明处开始,到代码块结束为止。

局部变量的作用域是变量所在的局部范围,不出大括号。

#include <stdio.h>

int main()
{
    int a = 6;
    {
        int a = 3;
        printf("%d\n",a);//3
    }//int a = 3 的作用域在大括号内

    printf("%d\n", a);//6

    return 0;
}

        全局作用域(Global Scope):全局作用域中声明的变量或函数可以在整个程序中访问,其作用域从声明处开始,到文件结束为止。

全局变量的作用域是整个工程

#include <stdio.h>

int a = 6;//全局变量

void test(void)
{
    printf("%d\n", a);
}

int main()
{
  
    test();//6

    printf("%d\n", a);//6

    return 0;
}

生命周期

变量的生命周期指变量的创建到变量的销毁之间的一个时间段

局部变量的生命周期:进入作用域生命周期开始,出作用域生命周期结束。

全局变量的生命周期:整个程序的生命周期。

字面常量

        字面常量(literal)是指代表固定值的标识符或常量,这些值在代码中是直接出现的,不需要计算或处理。字面常量可以是各种数据类型,如整数、浮点数、字符、字符串等。在程序编写时,字面常量通常用于赋值、比较或传递给函数等操作。

int num1 = 36;    // 十进制整数字面常量
int num2 = 075;   // 八进制整数字面常量(以0开头)
int num3 = 0x2A;  // 十六进制整数字面常量(以0x或0X开头)

float f1 = 3.14;      // 浮点数字面常量
double d1 = 6.022e23; // 科学计数法浮点数字面常量

char ch1 = 'A';       // 字符字面常量

char *str = "Hello, World!";  // 字符串字面常量
#include <stdio.h>

int main() 
{
    char* p = "hello";
    *p = 'H';//err
    printf("%s\n", p);
    return 0;
}

        这段代码存在一个严重的错误。尝试修改一个指向字符串常量的指针是未定义行为,因为字符串常量在大多数情况下是只读的。当尝试修改字符串常量时,会导致未定义行为,可能引发程序崩溃或其他意外结果。

在这个例子中,char* p = "hello";将字符串常量"hello"的地址赋给了指针p。然后,*p = 'H';尝试修改字符串常量的第一个字符为大写H,这是非法的。

如果您想要修改字符串的内容,可以使用字符数组而不是指向常量字符串的指针。下面是一个修改后的示例代码:

#include <stdio.h>

int main() 
{
    char str[] = "hello";
    str[0] = 'H';
    printf("%s\n", str);
    return 0;
}

const修饰的常变量

#include <stdio.h>

int main()
{
  
    const int a = 10;//在C中const修饰的a,本质是变量但不能被直接修改,有常量的属性。

    int* p = &a;//可用指针来指向该变量的地址

    *p = 6;//可通过地址解引用来修改这个const修饰的常变量

    printf("%d\n",a);//6

    return 0;
}

复合类型在定义时不会开辟空间

        结构体,联合体,枚举这些复合类型在定义时并不直接占用空间,而是在使用时分配空间。它们的大小取决于内部成员的大小和对齐方式,以及编译器的实现。通常在实例化这些类型的变量时才会占用实际的内存空间。

C语言中无字符串类型

        在C语言中,虽然没有内置的字符串数据类型,但是字符串通常被表示为以空字符('\0',ASCII值为0)结尾的字符数组。这种以空字符结尾的字符数组被称为C语言中的字符串。

        C语言中的字符串通常使用char类型的字符数组来表示,其中每个字符存储在数组的一个位置,直到遇到空字符为止。因此,C语言中的字符串实际上是一个以空字符结尾的字符数组。

#include <stdio.h>

int main()
{
  
    char arr1[] = "hello world";
    
    char arr2[] = { 'h','e','l','l','o',' ','w','o','r','l','d' };

    printf("%zu\n", sizeof(arr1));//12 字符串在末尾会多一个'\0'

    printf("%zu\n", sizeof(arr2));//11
    return 0;
}

0 ,'0','\0'

在C语言中,0'0''\0'分别代表不同的含义:

  1. 0

    0代表整数零。这是一个整型字面常量,表示数值零。
  2. '0'

    '0'代表字符零。这是一个字符字面常量,表示ASCII码为48的字符,即数字字符0。
  3. '\0'

    '\0'代表空字符(null character)。这是一个特殊的字符,ASCII码为0。在C语言中,空字符通常用来表示字符串的结尾。

#include <stdio.h>

int main()
{

    printf("%d\n", 0);    //0
    printf("%d\n", '0');  //48
    printf("%d\n", '\0'); //0

    return 0;
}

EOF(End-Of-File)

        在C语言中,EOF是一个宏,用于表示文件结束符(End-Of-File)。EOF通常用于标识文件结束或输入流的结束。

        在标准I/O库中,当读取文件或输入流时,EOF用于指示已达到文件末尾或输入流的末尾。EOF是一个负整数,通常被定义为-1

#include <stdio.h>

int main() {
    FILE *fp;
    int ch;

    // 打开文件
    fp = fopen("example.txt", "r");

    if (fp == NULL) {
        printf("File open error.\n");
        return 1;
    }

    // 读取文件内容
    while ((ch = fgetc(fp)) != EOF) {
        putchar(ch);
    }

    // 关闭文件
    fclose(fp);
    fp = NULL;    

    return 0;
}

不完全初始化

        在C语言中,不完全初始化是指对数组或结构体等复合数据类型的部分成员或元素进行初始化。这种情况下,未明确初始化的部分会根据C语言的规则自动初始化为0(或者对应数据类型的默认值)。

#include <stdio.h>

int main() {
    int arr[5] = {1, 2}; // 部分初始化数组

    // 输出数组元素
    for (int i = 0; i < 5; i++) {
        printf("%d ", arr[i]);
    }
    //输出结果是 1 2 0 0 0
    return 0;
}
#include <stdio.h>

int main() {
    
    char arr1[6] = { 'a','b','c' };
    char arr2[] = { 'a','b','c' };
    int i = 0;

    while (*(arr1+i))
    {
        
        printf("%c\n", arr1[i]);
        i++;
    }//打印出a,b,c

    printf("****************************\n");
    i = 0;

    while (arr2[i])
    {

        printf("%c\n", arr2[i]);
        i++;
    }//打印出a,b,c后还会打印出未知个数的未知字符


    return 0;
}

??) ,??(

 在一些老的编译器(VC6.0)在遇到 '??)'会转义成']','??('会转义成'['

转义字符

        转义字符是一种特殊的字符序列,用于表示非打印字符或具有特殊用途的字符。在C语言和许多其他编程语言中,转义字符以反斜杠\开头,后面跟着一个或多个字符,用于表示特定的含义。

以下是一些常见的转义字符及其含义:

  1. \n:换行符(newline),用于在输出中开始新的一行。
  2. \t:水平制表符(tab),用于在输出中进行水平制表对齐。
  3. \b:退格(backspace),用于删除前一个字符。
  4. \r:回车(carriage return),用于将光标移动到当前行的开头。
  5. \f:换页(form feed),用于在输出中执行换页操作。
  6. \\:反斜杠自身,用于表示反斜杠字符。
  7. \':单引号,用于表示单引号字符。
  8. \":双引号,用于表示双引号字符。
  9. \0:空字符(null),表示字符串的结束。
  10. \xhh:表示一个以十六进制值hh表示的字符。
  11. \ddd:八进制转义序列,其中ddd是一个八进制数,表示一个ASCII字符。
  12. \xdd 或 \xXX:十六进制转义序列,其中ddXX是一个十六进制数,表示一个ASCII字符。
  13. \a:是一个特殊的转义字符,它表示响铃字符(ASCII 值为 7)。当程序遇到 \a 转义字符时,通常会触发计算机发出一声蜂鸣或发出其他类似的提示音。
#include <stdio.h>

int main() {
    
 
    printf("c:\test\test.c\n");
    printf("c:\\test\\test.c\n");

    // 使用十六进制转义序列来输出字符串 "Hello"
    printf("\x48\x65\x6C\x6C\x6F\n");

    // 使用八进制转义序列来输出字符串 "Cat"
    printf("\103\141\164\n");

    // 使用十六进制转义序列来输出字符串 "Hello, World!"
    printf("\x48\x65\x6C\x6C\x6F\x2C\x20\x57\x6F\x72\x6C\x64\x21\n");

    // 输出响铃字符并换行
    printf("Beep sound: \a\n");
    printf("Beep sound: %c\n",7);

    return 0;
}

+,-,/

#include <stdio.h>

int main() {
    
 
    int a = -10;

    printf("%d\n", +a);//+a其实什么都不变,所以还是-10

    printf("%d\n", -a);//负负为正,所以是10
    
    int b = 11;
    printf("%d\n", b / 2);//除号的两端是整数时执行的是整数除法  5

    printf("%.1f\n", b / 2.0);//除号的两端只要一端是浮点数时执行的是浮点数除法 5.5

    printf("%d\n", b % 2);//取模操作符的两个操作数只能是整数 1

    return 0;
}

逗号表达式

        逗号运算符(,)用于分隔表达式,并按顺序执行这些表达式,最终返回逗号运算符右侧表达式的值。

逗号运算符主要用于以下几个方面:

#include <stdio.h>

int main() {
    int i = 0;

    // 在 while 循环中使用逗号表达式
    while (i++, printf("i = %d\n", i)) {
        
        if (i == 6)
        {
            break;
        }
    }

    return 0;
}

        1.在for循环中更新多个变量:逗号运算符可以在for循环中同时更新多个变量。

for (int i = 0, j = 10; i < 5; i++, j--) {
    // 循环体
}

        2.在表达式中简化代码:逗号运算符可以在一行代码中执行多个操作,使代码更紧凑。

int a = 5, b = 10, c;
c = (a++, b++, a + b); // c 等于 16,a 和 b 分别加 1 后相加

        3.函数调用:逗号运算符可以在函数调用时执行额外的操作。

int sum = addNumbers(2, 3), printResult(sum);

变长数组

        在C99标准中引入了变长数组(Variable Length Arrays,VLA),允许在函数内部声明具有可变长度的数组。这种数组的大小可以在运行时确定,而不是在编译时确定。使用VLA时,数组的长度可以根据需要在运行时动态地确定。

        变长数组的长度是在运行时确定的,这意味着无法在声明时对变长数组进行初始化。

#include<stdio.h>

int main()
{
	int n = 10;
	int numbers[n]; // 声明变长数组

	return 0;
}

 前/后置++,-- 

        在C语言中,++--是递增和递减运算符,它们可以分为前缀形式和后缀形式,分别是++aa++--aa--。这些运算符的行为有一些微妙的区别:

  • ++a(前缀递增):首先将a的值加1,然后返回递增后的值。
  • a++(后缀递增):首先返回a的当前值,然后再将a的值加1。
  • --a(前缀递减):首先将a的值减1,然后返回递减后的值。
  • a--(后缀递减):首先返回a的当前值,然后再将a的值减1。
#include <stdio.h>

int main() {
    int a = 5;

    // 前缀递增:先加1再使用
    printf("++a: %d\n", ++a); // 输出 6
    printf("a after ++a: %d\n", a); // 输出 6

    a = 5; // 重置 a 的值

    // 后缀递增:先使用再加1
    printf("a++: %d\n", a++); // 输出 5
    printf("a after a++: %d\n", a); // 输出 6

    a = 5; // 重置 a 的值

    // 前缀递减:先减1再使用
    printf("--a: %d\n", --a); // 输出 4
    printf("a after --a: %d\n", a); // 输出 4

    a = 5; // 重置 a 的值

    // 后缀递减:先使用再减1
    printf("a--: %d\n", a--); // 输出 5
    printf("a after a--: %d\n", a); // 输出 4

    return 0;
}
#include <stdio.h>

int main() 
{
    int a = 10;
    a++;
    ++a;
    int b = 0;
    b = 8;
    b--;
    --b;

    a = --b;
    b = a++;

    printf("%d %d\n", a, b);// 6 5
    return 0;
}

auto关键字

        在C语言中,auto关键字用于声明自动变量,这是C语言中本来就存在的默认存储类别,因此在现代C编程中,通常不需要显式地使用auto关键字。在C语言中,默认情况下,局部变量(在函数内部声明的变量)就是自动变量,它们在函数执行时被创建,在函数执行完毕时被销毁。

#include <stdio.h>

int main() {
    auto int a = 5; // auto关键字可以省略,a是自动变量
    int b = 10; // b也是自动变量

    printf("a: %d\n", a);
    printf("b: %d\n", b);

    return 0;
}

volatile

   volatile是一个类型修饰符,用于告诉编译器不要对被修饰的变量进行优化处理。通常情况下,编译器会对变量进行优化,例如将变量缓存在寄存器中,以提高程序的执行效率。但在某些情况下,这种优化可能会引起问题,特别是当变量的值可能在程序的控制之外被改变时。

主要情况包括:

  • 并行设备或硬件中,变量的值可能会由其他线程、中断服务程序或异步硬件修改。
  • 在多线程程序中,某个线程中的变量可能被其他线程修改。
  • 当变量表示某些特殊情况时,如在操作系统内核中表示硬件状态的寄存器。

每次访问volatile修饰的值时都要从它的地址中获取最新值。编译器会确保从该变量的地址中获取最新值,而不会使用之前缓存的值,这种行为对于需要与外部因素交互或受外部因素影响的变量非常重要。

typedef(类型定义 重命名)

   typedef关键字用于为现有数据类型创建新的类型别名,从而增加代码的可读性和可维护性。通过typedef,可以为复杂的数据类型或结构体定义更简洁的名称,使代码更易于理解。

#include<stdio.h>

typedef unsigned int u_int;

int main()
{
	u_int a = 3;
	printf("%zu\n", sizeof a);

	return 0;
}

static

        在C语言中,static关键字用于指定变量、函数或函数内部的局部变量的存储类别。static关键字的具体作用取决于它所修饰的对象的类型。

静态全局变量

        静态全局变量在整个程序运行期间都存在,但作用域仅限于声明它的文件。其他文件无法直接访问这个变量。

静态局部变量

        静态局部变量在程序执行过程中保持其值,不像普通局部变量在函数调用结束后被销毁。每次函数调用时,静态局部变量的值保持上一次调用结束时的值。

静态函数

        静态函数只能在声明它的文件中使用,无法被其他文件调用。这样可以限制函数的作用域,避免与其他文件中同名函数冲突。

        

switch语句的case标签  

        在C语言中,switch语句的case标签后面的值只能是整型常量表达式(如intcharenum等整型类型),并且这些值必须是唯一的。这是因为switch语句是根据case后面的值来匹配执行相应的代码块的,而这些值需要在编译时就能确定。

整型常量表达式case后的值必须是整型常量表达式,例如整数常量、字符常量、枚举常量等。

int x = 5;
switch (x) {
    case 1:
        // 代码
        break;
    case 'A':
        // 代码
        break;
    case RED:
        // 代码
        break;
    default:
        // 默认代码
}

唯一性case标签后的值在switch语句中必须是唯一的,不能重复。

int x = 2;
switch (x) {
    case 1:
        // 代码
        break;
    case 1: // 错误,重复的case值
        // 代码
        break;
    default:
        // 默认代码
}

不允许变量case后的值不能是变量,因为switch语句在编译时需要确定case值,而变量的值在运行时才确定。

int x = 2;
switch (x) {
    case x: // 错误,case值不能是变量
        // 代码
        break;
    default:
        // 默认代码
}

break

break语句,会立即跳出当前的循环,程序控制会继续执行循环后面的语句。

#include<stdio.h>

int main()
{
	
	while (1)
	{
		printf("外循环开头 ");

		while (1)
		{
			if (1)
			{
				printf("内循环 ");
				break;//跳出内循环
			}
		}

		printf("外循环结尾 ");
		break;//跳出外循环

	}

	return 0;
}

for(;;)

        在C语言中,for(;;)是一个无限循环的写法,相当于一个没有条件的for循环。这种写法等效于while(1)或者while(true),它会一直执行循环体中的代码直到遇到break语句或者程序被手动终止

#include<stdio.h>

int main()
{
	
	for (;;)
	{
		printf("abc ");//死循环
	}
	return 0;
}

函数递归

        函数递归指的是函数直接或间接调用自身的过程。在C语言中,函数递归是一种常见的编程技术,特别适合解决可以被分解为相似子问题的问题。当一个函数在执行过程中调用自身,这个过程就称为递归。

在函数递归中,存在两个要素:

  1. 基本情况(Base Case):递归函数中的一个条件,当满足这个条件时,递归不再继续,而是返回一个已知的结果。

  2. 递归情况(Recursive Case):递归函数中调用自身的条件,通过这个条件问题被分解为更小的相似问题。

goto语句

        在C语言中,goto语句是一种无条件跳转语句,允许程序跳转到代码中的标签(label)处执行。尽管goto语句在编程中被广泛视为不良实践,因为它可能导致代码变得难以理解和维护,但在某些情况下,使用goto语句可能是一种简洁有效的方法。

  1. 跳出多重循环:在多层嵌套循环中,当需要在内层循环中跳出外层循环时,使用goto可以是一种简洁的方法。

  2. 资源清理:在函数中遇到错误或异常情况时,需要释放已分配的资源(如内存),可以使用goto语句跳转到资源清理代码,避免重复释放资源的繁琐操作。

	for (...)
	{
		for (...)
		{
			for (...)
			{
				if (ERROR)
				{
					goto error;
				}
			}
		}
	}
    error:
	    //处理错误情况

        goto语句在C语言中通常只能在同一个函数内部跳转,无法跨越函数边界。这意味着goto语句只能在当前函数内部跳转到标签(label)所在的位置,而不能跳转到另一个函数中的标签。

形参,实参

        形参(formal parameter)是指函数定义中声明的参数,用于接收函数调用时传递的实际参数(实参)。形参在函数定义时用作变量名,在函数调用时,实参的值会被传递给形参,函数会使用这些值进行计算或处理。当函数调用完后形式参数就自动销毁了,因此形式参数只在函数中有效。

        实参是在函数调用时传递给函数的实际数值或变量。实参是传递给函数的实际数据,它们可以是常量、变量或表达式的值。实参的值会被传递给函数中对应的形参,以执行函数体中的操作。

传值/址调用

传值调用(Call by Value):

  • 传值调用是指将实参的值复制一份传递给函数的形参。在函数内部对形参的修改不会影响到实参的值。
  • 当使用传值调用时,函数操作的是复制的实参的值,而不是实参本身。
  • 传值调用适用于不希望函数修改实参的情况,同时能够保护实参的值不被函数改变。

传址调用(Call by Reference):

  • 传址调用是指将实参的地址(引用)传递给函数的形参,函数可以通过这个地址直接操作实参的值。
  • 在传址调用中,函数可以改变实参的值,因为函数操作的是实参的地址。
  • 传址调用适用于希望函数能够修改实参值的情况,或者希望通过函数返回多个值的情况。
#include <stdio.h>

// 传值调用
void callByValue(int x) {
    x = 20; // 修改形参的值
}

// 传址调用
void callByReference(int *x) {
    *x = 20; // 通过指针修改实参的值
}

int main() {
    int num1 = 10;
    int num2 = 10;

    callByValue(num1);
    printf("num1 after callByValue: %d\n", num1); // 输出:num1 after callByValue: 10

    callByReference(&num2);
    printf("num2 after callByReference: %d\n", num2); // 输出:num2 after callByReference: 20

    return 0;
}

数组传参实际上传递的是数组首元素的地址,而不是整个数组。

#include <stdio.h>

int arr_count(int arr[])//形参arr看着是数组,本质是指针变量
{
    //在函数内部计算参数部分的数组元素个数是不行的。
    return sizeof(arr) / sizeof(arr[0]); //x64环境   指针变量大小为8字节/int 类型大小4字节 = 2
}

int main() {
    
    int arr[6] = { 0 };
    printf("%d\n", sizeof(arr) / sizeof(arr[0]));//6

    printf("%d\n", arr_count(arr));//2

    return 0;
}

链式访问

把一个函数的返回值作为另一个函数的参数。

#include <stdio.h>

int main() {
    
    printf("%d", printf("%d", printf("%d", 43)));//4321

    //printf函数的返回值为打印数值的个数
    return 0;
}

main函数的参数

     main函数是程序的入口点,程序在执行时会从main函数开始运行。main函数可以有两种常见的形式:

标准形式:带有无参数的main函数,形式如下

int main() {
    // 函数体
    return 0; // 返回0表示程序正常结束
}

带参数的形式:带有命令行参数的main函数,形式如下

int main(int argc, char* argv[]) {
    // 函数体
    return 0; // 返回0表示程序正常结束
}

参数详解:
int argc:表示命令行参数的个数(argument count),即程序运行时通过命令行输入的参数个数,包括程序名称本身。
char* argv[]:是一个指向字符指针数组的指针(argument vector),存储了命令行参数的实际值,每个元素是一个指向以 null 结尾的 C 字符串的指针。argv[0]通常是程序的名称

这些参数主要用于以下目的:

  1. 接收命令行参数:通过main函数的参数argcargv,程序可以接收并处理在命令行中输入的参数。这在需要从命令行获取参数进行程序操作时非常有用。

  2. 传递程序名称argv[0]通常包含程序的名称或路径,可以用于获取程序自身的信息,比如程序所在的路径。

  3. 处理程序参数:命令行参数(argv[1]以及后续的参数)可以包含程序运行时需要的配置信息、文件名、选项等,程序可以根据这些参数来执行不同的操作。

  4. 控制程序行为:通过命令行参数,可以控制程序的行为方式,例如设置不同的模式、启用特定的功能或提供不同的输入。

  5. 增加程序的灵活性:通过命令行参数,可以使程序更加灵活和通用,用户可以在不改变程序源代码的情况下通过命令行参数来调整程序的行为。

二/多维数组

        二维数组是由多个一维数组组成的数组,通常用于表示表格或矩阵数据。在计算机编程中,二维数组可以被认为是一个由行和列组成的表格,相当于一维数组的数组。

#include <stdio.h>

#define arr3 ARR

int main() {

    int arr1[3][4] = { 1,2,3,4 };
    int arr2[3][4] = { {1,2},{3,4} };
    int arr3[][4] = { {1,2},{3,4},{5}};//二维数组初始化,行可以省略,列不能省略

    for (int i = 0; i < 3; i++)
    {
        for (int j = 0; j < 4; j++)
        {
            printf("%d ", ARR[i][j]);
        }
        printf("\n");
    }

    return 0;
}

        在C语言中,除了二维数组,还可以创建具有更多维度的多维数组。多维数组是数组的数组的概念,每个维度可以有不同的大小,形成多维的表格或矩阵。

#include <stdio.h>

int main() {
    int arr[][4][5] = {  //多维数组初始化只能第一个方括号内容可以省略
        {
            {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},
            {31, 32, 33, 34, 35},
            {36, 37, 38, 39, 40}
        },
        {
            {41, 42, 43, 44, 45},
            {46, 47, 48, 49, 50},
            {51, 52, 53, 54, 55},
            {56, 57, 58, 59, 60}
        }
    };

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

    return 0;
}
#include <stdio.h>

int main() {
   
    int arr[10] = { 0 };

    printf("%p\n", arr);//首元素地址
    printf("%p\n", arr + 1);//首元素地址 + 1个数组类型大小
    printf("\n");
    printf("%p\n", &arr[0]);//首元素地址
    printf("%p\n", &arr[0] + 1);//首元素地址 + 1个数组类型大小
    printf("\n");
    printf("%p\n", &arr);//数组地址
    printf("%p\n", &arr + 1);//数组地址 + 1整个数组的大小

    return 0;
}
#include <stdio.h>

int main() {
   
    int arr[3][4] = {0};

    printf("%zu\n", sizeof(arr) / sizeof(arr[0]));//3*4*4 / 4*4 = 3

    printf("%zu\n", sizeof(arr[0]) / sizeof(arr[0][0]));//4*4 / 4 =4

    printf("%p\n", arr);//数组首地址
    printf("%p\n", arr + 1);//数组首地址 + 列个数个数组类型大小(4*4) 
    printf("\n");
    printf("%p\n", arr[0]);//数组首地址
    printf("%p\n", arr[0] + 1);//数组首地址 + 1个数组类型大小(4)
    printf("\n");
    printf("%p\n", &arr);//数组地址
    printf("%p\n", &arr + 1);//数组首地址 + 1整个数组类型大小(3*4*4)
    printf("\n");
    printf("%p\n", &arr[0]);//数组首地址
    printf("%p\n", &arr[0] + 1);//数组首地址 + 列个数个数组类型大小(4*4) 
    printf("\n");
    printf("%p\n", &arr[0][0]);//数组首地址
    printf("%p\n", &arr[0][0] + 1);//数组首地址 + 1个数组类型大小(4)

    return 0;
}

移位操作符

        在计算机中,移位操作通常分为算术移位和逻辑移位。这两种移位操作的区别在于对待符号位的方式。左移(<<)和右移(>>)操作是通过CPU执行指令来完成的,而不是直接改变内存中的数据。这些移位操作通常是通过移位指令(shift instructions)来实现的,这些指令会将数据向左或向右移动指定的位数。

        当进行左移或右移操作时,CPU会从内存中加载数据到寄存器中,然后执行移位操作,赋值时将结果写回到寄存器或内存中。这些操作是基于位级别的,在整数运算中非常常见,可以高效地进行数据的位移动和位操作。

        左移和右移操作都是使用非负的整数值作为移动的位数。当使用负数作为移动位数时,可能会导致未定义的行为或错误。

int main() 
{
    int a = 10;
    
    a >> 1;
    printf("%d\n", a);//10

    a >>= 1;
    printf("%d\n", a);//5
    
    return 0;
}
  1. 逻辑移位

    • 对于无符号数,逻辑移位会在移位时在空出的位上填充0。
    • 逻辑左移:用0填充右侧空出的位。
    • 逻辑右移:用0填充左侧空出的位。
  2. 算术移位

    • 对于有符号数,算术移位会保持符号位不变,即符号位会随着移位操作一起移动。
    • 算术左移:用0填充右侧空出的位,符号位不变。
    • 算术右移:对于正数,用0填充左侧空出的位;对于负数,用1填充左侧空出的位,以保持符号位不变。

        移位操作符是用来在二进制数上执行位移操作的操作符。在C语言中,有左移操作符 << 和右移操作符 >>

  • 左移操作符 << 将一个数的二进制表示向左移动指定的位数,右侧空出的位用0填充。
     
    0010 << 1 = 0100
    
  • 右移操作符 >> 将一个数的二进制表示向右移动指定的位数,左侧空出的位根据最高位的符号位来填充(对于有符号整数,填充符号位;对于无符号整数,填充0)。
     
    0100 >> 1 = 0010
    

        这些操作符通常用于对整数进行快速乘法和除法运算,或者对位掩码进行操作。、

注意:移位操作符的操作数只能是整数

位操作符

        用于对整数的二进制位进行操作的运算符。常见的位操作符包括:

  1. 按位与(&):将两个操作数的对应位都设为1时,结果的对应位才为1,否则为0。
  2. 按位或(|):将两个操作数的对应位只要有一个为1时,结果的对应位就为1,否则为0。
  3. 按位异或(^):将两个操作数的对应位不相同时,结果的对应位为1,相同时为0。
  4. 按位取反(~):将操作数的每个位取反,即0变为1,1变为0。

注意:位操作数必须是整数

#include <stdio.h>

int main() {
    unsigned int a = 60; // 60 的二进制表示为 0011 1100
    unsigned int b = 13; // 13 的二进制表示为 0000 1101
    unsigned int result;

    // 按位与操作
    result = a & b; // 结果为 0000 1100,即十进制的 12
    printf("a & b = %u\n", result);

    // 按位或操作
    result = a | b; // 结果为 0011 1101,即十进制的 61
    printf("a | b = %u\n", result);

    // 按位异或操作
    result = a ^ b; // 结果为 0011 0001,即十进制的 49
    printf("a ^ b = %u\n", result);

    // 左移操作
    result = a << 2; // 结果为 1111 0000,即十进制的 240
    printf("a << 2 = %u\n", result);

    // 右移操作
    result = a >> 2; // 结果为 0000 1111,即十进制的 15
    printf("a >> 2 = %u\n", result);

    return 0;
}

三元运算符 ? :

        三目运算符,也称为条件运算符,是一种在许多编程语言中常见的运算符,用于根据条件的真假选择返回不同的值。其基本语法如下:

condition ? expression1 : expression2
  • 如果 condition 为真,则整个表达式的值为 expression1 的值。
  • 如果 condition 为假,则整个表达式的值为 expression2 的值。
#include <stdio.h>

int main() {
  
    int a = 3;
    int b = 0;

    printf("%d\n", (a > 5) ? (b = 3) : (b = -3));//-3
    printf("%d\n", (a > 5 ? 3 : -3));//-3

    return 0;
}

6[arr]

#include <stdio.h>

int main() {

    int arr[10] = { 0 };
   
    arr[6] = 8;//相当于*(arr + 6) = 8
    
    6[arr] = 9;//相当于*(6 + arr) = 9


    printf("%d\n",arr[6]);//9

    return 0;
}

        在 C 语言中,数组的元素可以通过下标访问,通常使用 array[index] 的形式,其中 array 是数组名,index 是要访问的元素的索引。在 C 语言中,数组名实际上是一个指向数组第一个元素的指针,因此 array[index] 和 index[array] 是等价的,因为在 C 语言中指针算术是合法的。

&&,||,!

        在C语言中,逻辑运算符 &&(逻辑与)、||(逻辑或)和!(逻辑非)用于执行逻辑运算。这些运算符通常用于布尔表达式,返回 true(非零) 或 false(0)。

  • &&(逻辑与):只有当两个操作数都为真时,表达式才为真。如果任一操作数为假,则表达式为假。
  • ||(逻辑或):只要有一个操作数为真,表达式就为真。只有当两个操作数都为假时,表达式才为假。
  • !(逻辑非):用于取反操作,如果操作数为真,则返回假;如果操作数为假,则返回真。

短路求值是指在逻辑运算中,如果表达式的结果可以确定,那么后续的表达式不会被计算。这种机制在使用逻辑运算符 &&(逻辑与)和 ||(逻辑或)时非常常见。

短路求值的规则:

  1. 逻辑与 &&

    • 如果第一个操作数为假(即 false 或 0),则不会计算第二个操作数,因为整个表达式的结果已经确定为假。
    • 只有在第一个操作数为真时,才会计算并返回第二个操作数的值。这种情况下,如果第二个操作数也为真,则整个表达式为真;否则为假。
  2. 逻辑或 ||

    • 如果第一个操作数为真(即 true 或非零),则不会计算第二个操作数,因为整个表达式的结果已经确定为真。
    • 只有在第一个操作数为假时,才会计算并返回第二个操作数的值。这种情况下,如果第二个操作数为真,则整个表达式为真;否则为假。
#include <stdio.h>

int main() {

    int i = 0, j = 0, a = 0, b = 2, c = 6;

    i = a++ && ++b && c++;//a = 1   左边为假右边就不计算

    j = ++a || b++ || ++c;//a = 2   左边为真右边就不计算

    printf("%d %d %d %d %d", i, j, a, b, c);//0 1 2 2 6 

    return 0;
}

函数操作符 () 

        在C语言中,函数操作符 () 用于调用函数。当你在代码中使用函数操作符并传递参数时,编译器会将其解释为对相应函数的调用。

Add(a,b);//()就是函数调用操作符,操作数为:Add,a,b  当函数没有传参数时,操作数为Add这一个

[]下标引用操作符

#include <stdio.h>

int main() {
  
    int arr[10];
    arr[6] = 10;//[]是下标引用操作符 []的两个操作数是arr和6

    //操作数 : 数组名  索引值

    return 0;
}

隐式类型转换

        隐式类型转换(Implicit Type Conversion)是指在表达式求值或赋值时,编程语言自动将一种数据类型转换为另一种数据类型,而无需显式地指定类型转换的操作。这种类型转换通常发生在不同数据类型之间,例如将整数转换为浮点数,或将字符转换为整数等。隐式类型转换有时也被称为自动类型转换。

在 C 语言中,隐式类型转换通常遵循一定的规则,如:

整型提升:

        在表达式中的字符和短整型操作数在使用之前被转换成普通整型(int)。因为CPU内运算器(ALU)的操作数的字节长度一般是int字节长度,同时也是CPU的通用寄存器的长度。在CPU执行算数操作时要先转换操作数为标准长度(int),所以小于int长度的整型值,都必须转换为int或unsigned int类型才能进入CPU执行运算。

#include <stdio.h>

int main() {
  
    char a = 0x80;
    unsigned short b = 0x8000;
    int c = 0x80000000;

    if (a == 0x80) //1000 0000(有符号整型提升补高位符号位)->1111 1111 1111 1111 1111 1111 1000 0000(4294967168)!= 1000 0000(128)
        printf("a");
    if (b == 0x8000)//1000 0000 0000 0000(无符号整型提升补0)->0000 0000 0000 0000 1000 0000 0000 0000(32768)== 1000 0000 0000 0000(32768)
        printf("b");
    if (c == 0x80000000)//不用整型提升 
        printf("c");
    //结果:bc
    return 0;
}

以下是整型提升的具体规则:

  1. 小整数类型提升为较大整数类型

    所有的char类型和short类型在表达式中会被自动提升为int类型。
  2. 根据容量提升为unsigned intsigned int

    • 如果int类型能够容纳所有可能取值的charshort类型,那么它们会被提升为signed int类型。
    • 如果int类型不能容纳所有可能取值的charshort类型,那么它们会被提升为unsigned int类型。
  3. 整型提升不会改变负数的值

    负整数以补码的形式进行整型提升,仍保持其负值。
  4. 整型提升不影响更大整数类型

    如果操作数已经是int类型或更大的整数类型(如longlong long),则不会进行整型提升。

整型提升是如何进行的?

整型提升是按照变量数据类型的符号位来进行提升的。

正数的整型提升:

如:

char num = 1;

char类型变量num的二进制位(补码)为8个比特位:00000001

char整型提升为int,高位补充符号位,正数补0,为:0000 0000 0000 0000 0000 0000 0000 0001

负数的整型提升:

如:

char num = -1;

char类型变量num的二进制位(补码)为8个比特位:11111111

char整型提升为int,高位补充符号位,负数补1,为:1111 1111 1111 1111 1111 1111 1111 1111 

无符号类型数的整型提升:

  unsigned char num = -1;

unsigned char类型变量num的二进制位(补码)为8个比特位:11111111

unsigned char整型提升为int,高位补0,为:0000 0000 0000 0000 0000 0000 1111 1111 (255)

#include <stdio.h>

int main() {
  
    unsigned char num = -1;
    
    printf("%d\n", num);//255

    return 0;
}

#include <stdio.h>

int main() {
  
    char a = 1;
    char b = -2;
    char c = 3;

    a = b + c;//低于int类型大小的操作数在要进行运算时其值会被提升为普通整型(int)再执行运算,运算完后将结果存截断存储到char类型的变量中。
              //b整型提升后的补码:1111 1111 1111 1111 1111 1111 1111 1110
              //c整型提升后的补码:0000 0000 0000 0000 0000 0000 0000 0011
              //b+c             :0000 0000 0000 0000 0000 0000 0000 0001
              //截断低8位       :0000 0001 (1) 
    printf("%d\n",b);
    return 0;
}
#include <stdio.h>

int main() {
  
    char a = 1;
    printf("%u\n", sizeof(a)); //1
    printf("%u\n", sizeof(+a));//4 只要参与表达式运算就会发生整型提升,所以是4个字节。
    printf("%u\n", sizeof(-a));//4 

    printf("%u\n", sizeof(++a));//1 ++a <=> a = a+1 已经完成了表达式的运算 提升的整型又被截断存储在char类型变量 所以是1个字节 
    printf("%u\n", sizeof(a++));//1
    printf("%u\n", sizeof(--a));//1
    printf("%u\n", sizeof(a--));//1

    return 0;
}

赋值转换:

  • 规则:当将一个值赋给另一个类型的变量时,如果两种类型不匹配,编译器会尝试进行隐式类型转换,将值转换为目标类型。
  • 例子int 赋值给 floatdouble 赋值给 int
    #include <stdio.h>
    
    int main() {
        float a = 9.5f;//常量后面加上 f 后缀来指示这是一个 float 类型的常量。如果不加后缀,则默认会被解释为 double 类型。
        int b = a; // 隐式类型转换,浮点数转换为整数
    
        printf("a: %f\n", a);
        printf("b: %d\n", b); // 输出 b 的整数值9
    
        return 0;
    }

函数调用中的类型转换:

  • 规则:如果函数的参数类型与传递的参数类型不匹配,编译器会尝试进行隐式类型转换,将实参转换为形参所需的类型。
  • 例子:函数声明为接收 int,但传递一个 double 类型的参数。

访问结构体成员

结构体指针->成员

结构体对象.成员

在 C 语言中,可以使用点运算符(.)和指针运算符(->)来访问结构体成员,具体如下:

点运算符(.

  • 点运算符通常用于直接访问结构体变量的成员。
  • 适用于结构体变量,而不是指向结构体的指针。
  • 用于访问结构体变量的成员时,结构体变量本身就是要操作的对象。

指针运算符(->

  • 指针运算符用于通过指针访问结构体的成员。
  • 适用于指向结构体的指针,允许通过指针间接访问结构体的成员。
  • 用于访问结构体指针指向的结构体的成员,指针本身存储了结构体的地址。
#include <stdio.h>

struct TEST
{
    int a;
    float b;
}T;

int main() {
  
    T.a = 10;
    T.b = 9.5f;//常量后面加上 f 后缀来指示这是一个 float 类型的常量。如果不加后缀,则默认会被解释为 double 类型。
    struct TEST* p = &T;
    p->a = 6;//(*p).a = 6;
    printf("%d\n",p->a);
    printf("%d\n", T.a);

    printf("%f\n", p->b);
    printf("%f\n", T.b);

    return 0;
}

表达式求值

        表达式求值是指根据表达式中运算符的优先级和结合性规则计算表达式的值。在 C 语言中,表达式求值遵循一定的规则,其中包括运算符的优先级和结合性。

以下是一些常见的 C 语言运算符按照优先级从高到低列出的详细列表:

  1. 括号

    ():函数调用、强制类型转换、表达式分组
  2. 一元运算符

    • ++--:前缀/后缀自增和自减
    • +-:一元加法和减法
    • !~:逻辑非和按位取反
    • *&:指针和取地址运算符
    • (type):类型转换
  3. 乘法、除法、取模

    • *:乘法
    • /:除法
    • %:取模(取余)
  4. 加法、减法

    • +:加法
    • -:减法
  5. 移位运算符

    • <<:左移
    • >>:右移
  6. 关系运算符

    • ><>=<=:大于、小于、大于等于、小于等于
    • ==!=:等于、不等于
  7. 按位与:&

  8. 按位异或:^

  9. 按位或:|

  10. 逻辑与:&&

  11. 逻辑或:||

  12. 条件运算符:? :

  13. 赋值运算符=+=-=*=/=%=&=|=^=<<=>>=

  14. 逗号运算符:,

        左结合性和右结合性指的是相同优先级的运算符在没有括号的情况下,是如何组合的。在 C 语言中,大多数运算符是左结合的,这意味着它们从左向右结合。例如,加法运算符 + 是左结合的,因此表达式 a + b + c 会被解释为 (a + b) + c

相比之下,赋值运算符 = 是右结合的,这意味着它们从右向左结合。例如,表达式 a = b = c 会被解释为 a = (b = c),即先将 c 的值赋给 b,然后将 b 的值赋给 a

int a = 10, b = 5, c = 3;
int result = a - b - c;
// 这个表达式涉及减法运算符 `-`,它是左结合的。
// 根据左结合性,表达式被解释为 `(a - b) - c`。
// 所以,先计算 `a - b`,然后再减去 `c`。
// 结果:2
int x = 10, y = 5, z = 3;
x = y = z;
// 这个表达式涉及赋值运算符 `=`,它是右结合的。
// 根据右结合性,表达式被解释为 `x = (y = z)`。
// 所以,先将 `z` 的值赋给 `y`,然后再将 `y` 的值赋给 `x`。
// 结果:x = 3, y = 3, z = 3
#include <stdio.h>

int main() {
  
    char a = 1;
    
    printf("%d\n", a + a--);// 操作符的优先级只能决定--在+的前面,而不知左操作符的获取在右操作符数之前还是之后求值,结果不可预测,有歧义。

    return 0;
}

        在C语言中,这段代码展示了一个比较有趣的情况,涉及到操作符的优先级、结合性和副作用问题。具体来说,这段代码中的表达式a + a--存在副作用和未定义行为,因为在同一表达式中修改同一变量的值并且使用该变量的值。这样的代码在C语言中是不确定的,因为C语言标准没有规定这种行为的执行顺序。

根据C语言标准,C语言不要求对于a + a--表达式中的a的求值顺序。这就导致了未定义行为。这种情况下,a + a--表达式的结果是不确定的,因为C语言标准没有定义在同一个表达式中修改同一个变量并使用该变量的值的行为。

因此,这段代码的输出结果是不确定的,编译器的不同实现或者编译器的优化等因素可能会导致不同的输出结果。在编写代码时,应该避免编写依赖于未定义行为的代码,以确保代码的可移植性和可靠性。

register

   register 是一个关键字,用来声明寄存器变量。在现代编译器中,register 关键字已经不再具有强制性,编译器会自行优化变量的存储方式。

  1. 用法

    register 关键字可以紧跟在变量声明之前,用于提示编译器将该变量存储在寄存器中。
  2. 存储位置

    寄存器是位于 CPU 内部的一种快速存储器,访问速度比内存快得多。因此,将变量存储在寄存器中可以提高访问速度。
  3. 限制

    • 不能对 register 变量使用取地址操作符 &,因为寄存器不是内存地址,只能直接在寄存器中访问变量。
    • register 关键字的使用是一个建议,编译器可以选择忽略这一建议。
  4. 优化

    现代编译器通常会根据情况自行优化变量的存储方式,因此 register 关键字并不像以前那样有着明显的性能优势。
#include <stdio.h>

int main() {
  
    register int x = 10;
    int* p = &x;//尝试对 register 变量取地址会导致编译错误

    return 0;
}

extern

        在 C 语言中,extern 关键字用于声明一个变量或函数,表明该变量或函数是在其他文件中定义的。当你在一个文件中使用 extern 声明一个变量时,编译器会知道该变量是在其他文件中定义的,并在链接阶段将其正确关联起来。

  extern 声明的变量本身并不会分配内存空间,而是表示该变量在其他地方定义了,它的实际内存分配发生在定义该变量的文件中。

通常情况下,在使用 extern 声明变量时,不应该初始化变量extern int a = 10。

正确的写法应该是:

extern int a;

格式化输出符号

  • %d: 以十进制有符号整数形式输出整数。
  • %u: 以十进制无符号整数形式输出整数。
  • %f: 以小数形式输出实数。
  • %lf: 以双精度浮点数形式输出实数。
  • %p: 以指针的形式输出地址。
  • %a: 以十六进制浮点数形式输出实数。
  • %lld: 以长长整型有符号整数形式输出整数。
  • %llu: 以长长整型无符号整数形式输出整数。
  • %n: 用于向参数列表的指定位置存储已写入字符的数目的整型指针。
  • %c: 输出一个字符。
  • %s: 输出字符串。
  • %x: 以十六进制形式输出整数。
  • %o: 以八进制形式输出整数。
  • %%: 输出一个百分号
  • %i: 用于输出整数,以十进制、八进制或十六进制形式表示,根据输入的格式而定。
  • %e%E: 用于以科学计数法输出浮点数,其中 %e 会输出指数为小写字母,而 %E 会输出指数为大写字母。
  • %g%G: 用于自动选择 %f 或 %e(或 %E)格式输出浮点数。对于较大或较小的浮点数,%g 会选择 %e(或 %E)以产生更紧凑的表示形式,而对于一般大小的浮点数,%g 会选择 %f 输出浮点数。
#include <stdio.h>

int main() {
    int integer = 42;
    unsigned int unsigned_integer = 100;
    float floating_point = 3.14;
    double double_precision = 12345.6789;
    char character = 'A';
    char string[] = "Hello, world!";
    long long int long_int = 9876543210;
    unsigned long long int unsigned_long_int = 1234567890;

    int num_chars = 0;

    printf("%%d: %d\n", integer);
    printf("%%u: %u\n", unsigned_integer);
    printf("%%f: %f\n", floating_point);
    printf("%%lf: %lf\n", double_precision);
    printf("%%p: %p\n", &integer);

    printf("%%a: %a\n", double_precision);
    printf("%%c: %c\n", character);
    printf("%%s: %s\n", string);
    printf("%%lld: %lld\n", long_int);
    printf("%%llu: %llu\n", unsigned_long_int);

    printf("%%x: %x\n", integer);
    printf("%%o: %o\n", integer);
    printf("%%%%: %%\n");

    printf("%%i: %i\n", integer);
    printf("%%e: %e\n", floating_point);
    printf("%%E: %E\n", floating_point);
    printf("%%g: %g\n", floating_point);
    printf("%%G: %G\n", floating_point);

    printf("Counting characters up to here: %n\n", &num_chars);//在老版编译器(VC6.0)支持
    printf("Number of characters printed: %d\n", num_chars);

    return 0;
}

int main() {

    char ch = 1;

    while (ch) 
    {
        scanf(" %n", &ch);//%c前加空格,跳过输入字符所有的空白字符
        printf("%c\n",ch);
    }
    
    return 0;
}

指针类型

        指针类型是一种特殊的数据类型,用于存储变量的内存地址。

指针类型决定了指针在被解引用的时候访问几个字节。

#include <stdio.h>

int main() {

    int a = 0x55667788;
    char* p1 = (char*)&a;//只访问1个字节
    *p1 = 0;

    printf("%#x\n", a);//0x55667700 低位的一个字节被修改为0

    int* p2 = &a;//访问4个字节
    *p2 = 0;

    printf("%#x\n", a);//0 整个int类型字节被修改为0

    return 0;
}

#include <stdio.h>

int main() {

    int a = 0x0000220a;//0000 0000 0000 0000 0010 0010 0000 1010

    char* p = (char*)&a;

    *p++ = 0;//*p = 0  p++  0000 0000 0000 0000 0010 0010 0000 0000(0x00002200) 0000 0000 0000 0000 0000 0000 0010 0010(0x00000022)

    printf("%#x\n",*p);//0x22

    short* pp = &a;

    pp++;//0000 0000 0000 0000 0010 0010 0000 1010 -> 0

    printf("%#x\n", *pp);//0

    return 0;
}

指针(Pointer):

  • 指针是一种数据类型,用于存储一个变量的内存地址。
  • 指针具体指向某种数据类型的变量。
  • 指针的声明包括指针类型和指针变量名,例如 int *ptr;
  • 指针可以进行算术运算和间接引用(解引用)操作,以访问指向的内存地址中的数据。
  • 指针的类型决定了指针可以指向的数据类型,例如 int * 指向整数,char * 指向字符等。

void * 指针:

  • void * 是一种特殊类型的指针,被称为“无类型指针”。
  • void * 指针可以存储任何类型的数据的地址,因为它是一种通用指针。
  • void * 指针可以用来传递内存地址而不关心具体的数据类型,或者用来实现一些通用的数据结构。
  • void * 指针不能直接进行指针运算或解引用操作,因为它没有指定具体的数据类型。
  • 在使用 void * 指针时,通常需要将其转换为特定的指针类型才能访问或操作指向的数据。

总结:

  • 指针是指向特定类型的数据的地址的变量,而 void * 是一种通用指针,可以指向任何类型的数据。
  • 指针具有类型信息,而 void * 指针是无类型的,需要在使用时进行类型转换。
  • 指针可以直接操作指向的数据,而 void * 指针需要先转换为具体类型的指针才能访问数据。

        在实际编程中,void * 指针通常用于实现通用的数据结构或接口,以便处理各种类型的数据。指针则主要用于直接访问和操作特定类型的数据。

#include <stdio.h>

int main()
{
	char* c[] = { "ENTER","NEW","POINT","FIRST" };
	char** cp[] = { c + 3,c + 2,c + 1,c };
	char*** cpp = cp;

	printf("%s\n", **++cpp);//++cpp相当于cpp=cpp+1 改变量cpp的值							POINT
	printf("%s\n", *-- * ++cpp + 3);//先算++cpp cpp=cpp+1+1 然后 * 然后 -- 然后 * 最好 +3   ER
	printf("%s\n", *cpp[-2] + 3);//*cpp[-2] <=> * *(cpp-2) cpp:cpp+1+1-2                    ST

	printf("%s\n", cpp[-1][-1] + 1);//cpp[-1][-1] <=> *(*(cpp-1)-1) cpp:cpp+1+1-1           EW      

	return 0;
}

空指针,野指针

  1. 空指针(Null Pointer)

    • 空指针是指不指向任何有效对象或函数的指针。
    • 在C语言中,空指针用宏 NULL 表示,通常定义在头文件 <stddef.h> 或 <stdio.h> 中。
    • 空指针通常用于表示指针不指向任何有效的内存地址,可以在程序中初始化指针,或者作为函数的返回值来表示某种特殊情况。
    • 不能访问空指针,因为它不指向任何有效的内存地址。
      #include <stdio.h>
      
      int main() {
      
          int* p = NULL;
          *p = 2;//err
          printf("%d\n",*p);
      
          return 0;
      }

      上面代码存在对空指针 p 进行解引用的错误。解引用空指针会导致未定义行为,因为空指针不指向有效的内存地址。这种操作可能会导致程序崩溃或产生意外的结果。

      为了避免这种问题,应该在解引用指针之前确保指针指向了有效的内存地址。

  2. 野指针(Dangling Pointer)

    • 野指针是指指向已经释放或无效的内存地址的指针。
    • 当指针指向的内存被释放或者超出了作用域,但指针本身并没有被重置或更新,这时就会形成野指针。
    • 使用野指针可能导致程序崩溃、未定义行为或数据损坏,因为它指向的内存可能已经被其他程序使用或系统回收。
    • 为了避免野指针的问题,应该确保指针始终指向有效的内存地址,或者在释放内存后将指针置为NULL。
      #include <stdio.h>
      
      int* test(int n)
      {
          int a = n;//a是局部变量 调用完就销毁了 
          return &a;
      }
      
      int main() {
      
          int* p = test(10);//指针p保存着a的地址 但a已经被销毁了 p就变了成野指针
      
          printf("hello world!");//干扰一下栈区内容
          printf("%d\n",*p);//输出大概率不会是10
      
          return 0;
      }

指针-指针

        在C语言中,可以对指针进行减法操作,以计算两个指针之间的偏移量(以元素为单位)。这种操作通常用于计算数组中两个元素之间的距离,或者计算指针指向的内存区域的大小。指针之间的减法操作返回的结果是一个整数,表示两个指针之间相隔的元素个数。

#include <stdio.h>

int main() {

    int arr[10] = { 0 };
    char* p1 = arr;
    int* p2 = arr;

    printf("%d\n", &p1[9] - &p1[0]);//9   指针跟指针之间元素的个数
    printf("%d\n", &p2[9] - &p1[0]);//9

    printf("%d\n", &p1[9] - &p2[0]);//9
    printf("%d\n", &p2[9] - &p2[0]);//9

    return 0;
}

          指针相减的两个指针必须指向同一块内存区域:如果两个指针指向不同的内存块,或者它们之间的偏移量超出了合法范围,那么指针相减的结果是未定义的。

        在 C 语言中,允许将指向数组中最后一个元素后一个位置的指针(即超出数组范围的指针)与指向数组内元素的指针进行比较,但是不允许将指向数组第一个元素之前一个位置的指针与指向数组内元素的指针进行比较。 

#include <stdio.h>

int main() {
 
	int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
	int i = 0;
	int n = &i - &arr;//计算出地址相差的元素个数
	

	for (i = 0; i <= n; i++)//n>9时越界赋值访问 
	{
		if (&arr[n] == &i)
		{
			printf("&arr[n] == &i\n");
		}
		arr[i] = 0; //因为 &arr[n] == &i 所以当i=n时arr[n]=0 相当于赋值i=0 这就造成了死循环
		printf("嘻嘻 ");
	}

	return 0;
}

 二级指针        

        在 C 语言中,二级指针(double pointer)是指一个指针,该指针指向另一个指针的地址,而被指向的指针再指向实际的数据或对象。二级指针通常用于传递指针的指针,或者用于动态内存分配中的一些情况,比如函数中修改指针的指向等。

#include <stdio.h>

int main() {
 
    int a = 10;
    int* p1 = &a;
    int** p2 = &p1;//int* 说明p2指向的对象是int*类型 还有一个*说明p2是一个指针 

    **p2 = 6;//*p2为a的地址 **p2为p1的值

    printf("%p\n", *p2);
    printf("%p\n",  p1);

    return 0;
}

指针数组

        指针数组是一个数组,其元素都是指针。每个指针指向内存中的某个位置,可以是变量、数组、函数等。

#include <stdio.h>

int main() {
 
	int arr1[3] = { 1,2,3 };
	int arr2[3] = { 3,2,1 };
	int arr3[3] = { 5,6,7 };

	int* parr[3] = { arr1,arr2,arr3 };

	for (int i = 0; i < 3; ++i)
	{
		for (int j = 0; j < 3; j++)
		{
			printf("%d ", parr[i][j]);//parr[i][j] <=> *(*(parr+i)+j)
			printf("%d ", *(*(parr + i) + j));
		}
		printf("\n");
	}
}

数组越界改变控制变量导致死循环

#include <stdio.h>

int main() {
 
	int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
	int i = 0;
	int n = &i - &arr;//计算出地址相差的元素个数
	

	for (i = 0; i <= n; i++)//n>9时越界赋值访问 
	{
		if (&arr[n] == &i)
		{
			printf("&arr[n] == &i\n");
		}
		arr[i] = 0; //因为 &arr[n] == &i 所以当i=n时arr[n]=0 相当于赋值i=0 这就造成了死循环
		printf("嘻嘻 ");
	}

	return 0;
}

无符号常量最好带有字母u后缀

       在C/C++中,无符号常量最好带有字母u后缀,以明确指示该常量是无符号的。这有助于提高代码的可读性和可维护性。通常,无符号整数常量应该以uU结尾,以便清楚地表明它们的数据类型。

例如,在声明无符号整数常量时,可以这样写:

const unsigned int MAX_VALUE = 100u;
const unsigned long long BIG_NUMBER = 1000000000ULL;

在这里,uU后缀表示常量是无符号的。同样,对于更大的整数类型,如unsigned long long,使用ULLull后缀也是一种良好的做法。

带有后缀的无符号常量有助于代码的可读性,避免误解,并且可以帮助编译器正确地推断常量的数据类型,从而避免潜在的数据类型转换问题。

浮点数的精度损失

浮点数精度损失是指在计算机中表示浮点数时可能出现的精度缺失或误差。这种损失是由于计算机使用有限数量的位来表示实数值,而实数值本身可能是无限精度的。

主要原因:

  1. 有限精度表示:计算机使用有限数量的位来表示浮点数,因此无法准确表示所有实数值,尤其是无理数或无限循环小数。

  2. 舍入误差:在进行浮点数运算时,会出现舍入误差。由于计算机存储的位数是有限的,计算结果可能需要舍入到最接近的可表示值,导致精度损失。

  3. 浮点数格式:浮点数通常使用 IEEE 754 标准来表示,其中包括尾数和指数部分,这也会导致精度损失。

如何处理精度损失:

1. 避免直接比较浮点数

避免直接比较两个浮点数是否相等,因为由于精度问题可能会导致结果不准确。应该比较它们的差值是否在某个可接受的误差范围内。

if (fabs(a - b) < EPSILON) {
    // a 和 b 在可接受的误差范围内相等
}

2. 使用整数计算

在某些情况下,可以将浮点数转换为整数进行计算,然后再将结果转换回浮点数。这样可以减少精度损失的可能性。

3. 使用高精度计算库

对于需要高精度计算的应用,可以使用专门的高精度计算库,如 GNU MPFR(Multiple Precision Floating-Point Reliable)库,它提供了高精度的浮点数计算能力。

4. 避免连续浮点数操作

尽量避免在连续操作中多次累积浮点数误差。如果可能的话,尽量将计算重新组织为更精确的方式。

5. 理解浮点数表示

深入了解浮点数的表示方式、精度限制和舍入误差的来源对于避免精度损失很有帮助。这有助于更好地预测和处理潜在的精度问题。

6. 使用定点数

在某些情况下,使用定点数代替浮点数也是一种解决方案,因为定点数在某些场景下可以提供更可控的精度。

#define _CRT_SECURE_NO_WARNINGS

#include <stdio.h>
#include <math.h>
#include <float.h>

#define EPSILON 0.000001

int main() {
 
	double x = 1.0;
	double y = 0.1;
	printf("%.50lf\n", x);
	printf("%.50lf\n", y);
	printf("%.50lf\n", x-y);

	if (y == 0.1)
	{
		printf("ok\n");
	}
	else
	{
		printf("no\n");
	}

	if (x - y == 0.9)
	{
		printf("x-y=0.9\n");
	}

	if (x - y - 0.9 < EPSILON && x - y - 0.9 > -EPSILON)
	{
		printf("EPSILON: x-y=0.9\n");
	}

	if (fabs(x - y - 0.9) < EPSILON) 
	{
		printf("fabs: x-y=0.9\n");
	}

	if (fabs(x - 0.9 - y) < DBL_EPSILON) 
	{
		printf("DBL\n");
	}

	return 0;
}

强制类型转换

        在C语言中,强制类型转换是一种将一个数据类型转换为另一个数据类型的操作。这种转换可以在某些情况下是必要的,但需要小心使用,因为错误的类型转换可能导致未定义的行为或错误的结果。

注意事项:

  1. 精度丢失

    当将一个高精度的数据类型转换为低精度的数据类型时,可能会导致精度丢失。例如,将 double 类型转换为 int 类型时,小数部分将被截断。
  2. 符号扩展或截断

    在有符号和无符号数据类型之间进行转换时,可能会发生符号扩展或截断。这可能导致意外的结果,特别是当负数被转换为无符号类型时。
  3. 指针转换错误

    不正确的指针类型转换可能会导致程序崩溃或未定义的行为。确保在进行指针转换时,目标类型与实际数据类型兼容。
  4. 类型不匹配

    将不兼容的类型进行强制转换可能会导致数据损坏或错误的结果。例如,将指针转换为整数类型,或者将整数转换为指针类型。
  5. 逻辑错误

    有时,类型转换可能会掩盖代码中的逻辑错误。例如,将一个错误类型的值转换为另一种类型,可能会隐藏数据处理中的逻辑错误。
  6. 多次转换

    连续进行多次类型转换可能会导致数据失真。确保在进行多次转换时了解每次转换的影响,以避免数据丢失或不正确的结果。
  7. 无意义的转换

    进行无意义的类型转换可能会增加代码的复杂性并降低可读性。避免不必要的类型转换,只在确实需要时进行转换。
  8. 类型大小不匹配

    当进行转换时,确保目标类型足够大以容纳源类型的值。否则可能会导致数据溢出或截断。
#include <stdio.h>

int main()
{
	float a = 3.16f;
	printf("%d\n", (int)a);  //3.16 会被转换为 3,因为强制类型转换会将小数部分直接截断。

	printf("%d\n", (int)'1');//ASCII码中 '1' 对应的整数值是 49。

	printf("%d\n", (int)"123456");//err 将一个字符串字面值强制转换为整数是不正确的,因为字符串字面值本身并不是整数。
	                              //这可能会导致编译器报错或输出不确定的结果。

	printf("%d\n", 0);    //打印整数0,输出为 0。
	printf("%d\n", '\0'); //打印字符\0对应的整数值,即ASCII值为0,输出为 0。
	printf("%d\n", NULL); //打印NULL对应的整数值,NULL通常定义为((void*)0),输出为 0  

	return 0;
}

强制类型转换并不改变该数据在内存中的二进制

        C语言中,强制类型转换并不会改变数据在内存中的二进制表示。强制类型转换只是告诉编译器将某种数据类型视为另一种数据类型,但实际上并不修改数据本身。

当进行强制类型转换时,编译器会根据转换的类型规则重新解释数据,并生成适当的指令来处理数据。这可能导致数据在表达和使用上的不同,但并不会修改数据在内存中的实际表示。        

#include <stdio.h>

int main()
{
	float a = 3.16f;
	int b = (int)a;

	printf("a=%f b=%d\n", a, b);//a=3.160000 b=3

	return 0;
}

真实的转换 和 强制类型转换

        真实的转换要改变内存中的数据。

        强制类型转换不改变内存中的数据,只改变对应的类型。

switch case 

   switch语句和if-else语句在C语言中通常用于根据不同条件执行不同的代码块,但它们之间有一些重要的区别:

  1. 条件类型

    • if-else语句:用于基于布尔表达式的条件执行代码块。条件可以是任何返回布尔值的表达式。
    • switch语句:用于基于整数值或字符值的条件执行代码块。switch语句中的表达式的结果必须是整数或字符。
  2. 多条件处理

    • if-else语句:适合处理多个不同的条件,每个条件可以是不同的布尔表达式。
    • switch语句:适合处理多个固定值的情况,每个case中的值与switch表达式进行比较。
  3. 跳转性

    • if-else语句:在满足条件后,执行对应的代码块,然后程序流程继续向下执行。
    • switch语句:在找到匹配的case后执行对应的代码块,然后使用break语句跳出switch块,如果没有break,会继续执行后续的case,直到遇到breakswitch结束。
  4. 可读性

    • switch语句:当需要根据一个表达式的值来进行多个选择时,switch语句可能更加清晰和易读。
    • if-else语句:适用于更复杂的条件逻辑,例如需要使用逻辑运算符连接多个条件时。

总的来说,if-else语句更加灵活,适用于处理不同的条件表达式,而switch语句更适用于处理固定值的情况,提高代码的可读性和可维护性。

#include <stdio.h>

int main()
{
	int a = 0;
	scanf("%d\n", &a);

	switch (a)
	{
	case 1:
		printf("%d\n", a);
		break;
	case 2:
		printf("%d\n", a);
		break;
	case 3:
		printf("%d\n", a);
		break;
	default:
		int b = a;//err
		printf("%d\n", b);
		break;
	}

	return 0;
}
#include <stdio.h>

int main()
{
	int a = 0;
	scanf("%d", &a);

	switch (a)
	{
	case 1:
		printf("%d\n", a);
		break;
	case 2:
		printf("%d\n", a);
		break;
	case 3:
		printf("%d\n", a);
		break;
	default:
		{
		int b = a;
		printf("%d\n", b);
		break;
		}
	}

	return 0;
}

default可以放在switch中的任意位置

#include <stdio.h>

int main() {
    int month;
    printf("Enter a month number (1-12): ");
    scanf("%d", &month);

    switch (month) {
    case 1:
    case 3:
    case 5:
    default:
        printf("Invalid month number\n");
    case 7:
    case 8:
    case 10:
    case 12:
        printf("31 days\n");
        break;
    case 4:
    case 6:
    case 9:
    case 11:
        printf("30 days\n");
        break;
    case 2:
        printf("28 or 29 days\n");
        break;
   
    }

    return 0;
}

   switch语句中的case标签后必须是一个常量表达式,这是C语言的语法规定。这意味着case后面的表达式必要么是一个常量,要么是一个常量表达式。这是因为switch语句在编译时需要知道每个case标签的值,以便进行匹配。

#include <stdio.h>
#define c 6

int main() 
{
    int a = 0;
   const int b = 0;

   switch (a)
   {
       case a://err必须是常量
       case b://err必须是真常量
       case c://ok
   default:
	   break;
   }

    return 0;
}

        通常将执行频率较高的case放在前面,以提高switch语句的效率。这样可以减少不必要的比较操作,因为switch语句会从上往下依次执行case,直到找到匹配的情况为止。

键盘输入的内容以及显示屏打印的内容全部都是"字符"(字符设备)

#include <stdio.h>

int main() 
{
    int ret = printf("%d\n", -1234);//-1234 看似是整数 其实是一个字符一个字符打印出来的

    printf("%d\n", ret);//6 : - 1 2 3 4 \n 这6个字符

    return 0;
}

while,for循环中continue的跳转

 while(){} ,do{}while(); continue都是跳转到条件判断处。


#include <stdio.h>
#include <windows.h>

int main() 
{
    int i = 10;

    while(i) //continue 是跳转到条件判断处  do-while也是会死循环
    {

        Sleep(1000);
        printf("continue before:%d\n", i);

        if (6 == i)
        {
            printf("continue:%d\n", i);
            continue;
        }

        printf("continue after:%d\n", i);

        --i;
     
    }
   
    return 0;
}

for是跳转到条件更新处。

#include <stdio.h>

int main() 
{
    int i = 0;

    for (i = 0; i < 10; i++) //continue 是跳转到条件更新处  不会死循环
    {
        printf("continue before:%d\n", i);

        if (6 == i)
        {
            printf("continue:%d\n", i);
            continue;
        }

        printf("continue after:%d\n", i);
        
    }
   
    return 0;
}

void

   void在C语言中用于表示空或无类型,可用于函数返回类型、指针类型、函数参数类型以及空指针的定义。

void被编译器解释为空类型,不允许定义变量。

#include <stdio.h>

int main() 
{
    //void x;//err

    printf("%d\n", sizeof(void));//0

    return 0;
}

void* 可以接收任意指针类型以及被任何类型的指针接收

#include <stdio.h>

int main() 
{
    char* a = NULL;
    short* b = NULL;
    int* c = NULL;
    float* d = NULL;
    double* e = NULL;

    void* p = a;
    b = p;

    return 0;
}

不允许对 void* 指针进行算术运算以及进行解引用操作

#include <stdio.h>

int main() 
{
  
    void* p = NULL;
    p++;//err 不允许对 void* 指针进行算术运算
    p--;//err

    *p = 3;//err 不允许对 void* 指针进行解引用操作

    return 0;
}

但在Linux中可以进行void* 指针加减,因为在windows中void* 指针大小为0,在linux中void* 指针大小为1。

root@iZ2vch4tdjuyi8htrm9i7hZ:~/test# cat 1.c
#include <stdio.h>

int main()
{
   printf("%d\n",sizeof(void));//1

   void* p = NULL;

   printf("%d\n",(int*)(++p));//1

   printf("%d\n",(int*)(--p));//0

   return 0;
}
root@iZ2vch4tdjuyi8htrm9i7hZ:~/test# ./a.out
1
1
0
root@iZ2vch4tdjuyi8htrm9i7hZ:~/test#

return

        众所周知在调用函数时会在栈上形成函数栈帧结构开辟对应的空间,在函数栈帧结构里的是在函数内定义的局部变量。当函数调用完后,函数栈帧结构就被释放了,里面的局部变量自然也是。

#include <stdio.h>

char* show()
{
    char arr[] = "hello world";
    return arr;//err 返回的地址空间在函数调用完毕后会被释放
}

int main() 
{
    char* s = show();

    printf("%s\n", s);//当调用打印函数时,会对先前show()函数形成的栈帧结构进行覆盖,因为该函数已经被释放了。
    
    return 0;
}

函数的返回值,通过寄存器返回给函数调用方,即使被释放了但值保存到寄存器进行获取。

#include <stdio.h>

int show()
{
    int a = 10;
    return a;
}

int main() 
{
    int s = show();

    printf("%d\n", s);
    
    return 0;
}

将存储在变量a的内存地址处的值加载到eax寄存器中

        调用了名为 show 的函数。call 指令用于调用函数,将程序控制权转移到函数所在的地址,并在函数返回时将控制权返回到调用点。然后将 eax 寄存器中的值存储到变量 s 的内存地址中。

        在C++中,main() 函数的返回值实际上是返回给调用main()函数的执行环境,通常是操作系统。操作系统会检查main()函数的返回值,该返回值通常用来表示程序的运行状态,成功与否等信息。

        一般来说,main()函数的返回值会作为程序的退出状态码。通常约定,返回0表示程序成功执行,而非0的返回值通常用来表示某种错误或异常情况。这些返回值可以被操作系统捕获并进一步处理,比如在脚本中根据程序的返回值采取不同的行动。

任何一个变量名,在不同的应用场景代表不同含义

#include <stdio.h>

int main() 
{
    int x;
    
    x = 100;  //这里x表示空间变量属性,属于左值。

    int y = x;//这里x表示内容数据属性,属于右值。
    
    return 0;
}

在c中任何变量取地址都是最低地址开始

关键字对关键字不起作用

#include <stdio.h>

int main() {
    const int num = 100;
    const int* ptr1 = &num;
    const int** ptr2 = &ptr1;
    const int  * const* const p = ptr2;

    // 尝试修改指针指向的内容会导致编译错误
    //**p = 200;

    // 尝试修改指针指向的地址会导致编译错误
    //*p = NULL;

    // 尝试修改指针本身会导致编译错误
    //p = NULL;

    printf("Value pointed by p: %d\n", **ptr2);

    return 0;
}

任何函数传参都要形成临时变量,包括指针变量。

#include <stdio.h>

void show(const int* p)//新开辟的指针变量p,用来存函数传过来a的地址。
{
    printf("%p\n", p); //a的地址
    printf("%p\n", &p);//新开辟的指针变量p的地址
}

int main() 
{
    int a = 10;
    printf("%p\n", &a);//a地址
    show(&a);

    return 0;
}
#include <stdio.h>

const int getValue() {
    return 6;
}

int main() {
    const int result = getValue();

    // 尝试修改返回值会导致编译错误
    // result = 10;

    printf("Returned value: %d\n", result);

    return 0;
}

extern

        在 C 语言中,extern 是一个关键字,用于声明一个变量或函数是在其他文件中定义的,即该变量或函数在当前文件中是外部可见的。通过使用 extern 关键字,可以在一个文件中声明一个在另一个文件中定义的全局变量或函数,从而避免重复定义。

1. 外部变量的声明

// 在文件A中声明外部变量,不分配存储空间
extern int globalVariable;

// 在文件B中定义外部变量
int globalVariable = 100;

2. 外部函数的声明

// 在文件A中声明外部函数
extern void externalFunction();

// 在文件B中定义外部函数
void externalFunction() {
    // 函数实现
}

3. 使用外部变量

// 在另一个文件中使用外部变量
extern int globalVariable;

int main() {
    // 使用外部变量
    printf("Global variable value: %d\n", globalVariable);
    return 0;
}

通过 extern 关键字,可以使得外部定义的变量或函数在当前文件中可见,从而实现模块化的代码组织和避免重复定义。extern 常常与头文件一起使用,头文件中声明外部变量和函数,而在实现文件中定义它们。

结构体和数组只能整体被初始化不能整体赋值

        在 C 语言中,结构体和数组都可以被整体初始化和赋值,但数组可以用函数整体赋值。

结构体的整体初始化和整体赋值:

  1. 结构体的整体初始化

    • 结构体可以在定义时进行整体初始化,或者在声明结构体变量时进行整体初始化。
    // 结构体的整体初始化
    struct Point {
        int x;
        int y;
    };
    
    struct Point p = {10, 20};  // 整体初始化
    
  2. 结构体的整体赋值

    • 结构体的成员只能逐个赋值,而不能通过整体赋值来初始化整个结构体。
    // 结构体的整体赋值是非法的
    struct Point p;
    // p = {10, 20};  // 错误,无法整体赋值
    p.x = 10; p.y = 20;  // 逐个赋值
    

数组的整体初始化和整体赋值:

  1. 数组的整体初始化

    • 数组可以在定义时进行整体初始化,指定每个元素的值
    int arr[5] = {1, 2, 3, 4, 5};  // 整体初始化数组
    
  2. 数组的整体赋值

    • 数组可以通过 memcpy 等函数进行整体赋值,但不能直接使用赋值运算符整体赋值。
    int arr1[5] = {1, 2, 3, 4, 5};
    int arr2[5];
    
    // 通过 memcpy 进行整体赋值
    memcpy(arr2, arr1, sizeof(arr1));

柔性数组

        在 C 语言中,柔性数组(Flexible Array Member)通常定义在结构体的最后一个成员位置,且前面至少应该有一个成员变量。通常用于创建可变大小的结构体。        

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

// 定义包含柔性数组的结构体
struct FlexArray {
    int length;  // 柔性数组前最好至少有一个变量
    int data[];  // 柔性数组成员,大小为0
};

int main() {
    int n = 5;  // 假设数组长度为5

    // 分配内存以容纳柔性数组和结构体本身
    struct FlexArray* fa = malloc(sizeof(struct FlexArray) + n * sizeof(int));

    if (fa == NULL) {
        fprintf(stderr, "Memory allocation failed\n");
        return 1;
    }

    fa->length = n;

    // 对柔性数组赋值
    for (int i = 0; i < n; i++) {
        fa->data[i] = i * 10;
    }

    // 访问柔性数组
    for (int i = 0; i < fa->length; i++) {
        printf("fa->data[%d] = %d\n", i, fa->data[i]);
    }

    // 释放动态分配的内存
    free(fa);

    return 0;
}

联合体(union)

        在 C 语言中,联合体(union)是一种特殊的数据结构,它允许在同一块内存中存储不同类型的数据,但同一时间只能存储其中的一个成员。联合体的大小等于其最大成员的大小。

联合体内,所有成员的起始地址都是一样的,在低地址处。

#include <stdio.h>

union un
{
    char c;
    int b;
};

int main()
{
    union un u;

    printf("%d\n", sizeof(union un));

    printf("%p\n", &u);
    printf("%p\n", &(u.c));
    printf("%p\n", &(u.b));

    return 0;
}
#include <stdio.h>

union un
{
    char b;
    int a;
};

int main()
{
    union un u;

    u.a = 0x00000001;
    
    if (u.b == 1)
    {
        printf("小端");
    }
    else if (u.b == 0) 
    {
        printf("大端");
    }

    return 0;
}

typedef

        在C语言中,typedef 是一个用来为已有的数据类型定义新的名称的关键字。通过 typedef,可以为已有的数据类型(如基本数据类型、结构体、枚举等)创建新的别名,从而增加代码的可读性和可维护性。

typedef 的基本语法:

typedef existing_type new_type_name;

示例说明 typedef 的使用:

  1. 基本数据类型的 typedef

    typedef unsigned int uint; // 为 unsigned int 定义一个别名 uint
    typedef char * string;     // 为 char* 定义一个别名 string
    
  2. 结构体的 typedef

    typedef struct {
        int x;
        int y;
    } Point; // 为结构体定义一个别名 Point
    
  3. 枚举的 typedef

    typedef enum {
        RED,
        GREEN,
        BLUE
    } Color; // 为枚举定义一个别名 Color
    
  4. 函数指针的 typedef

    typedef int (*MathFunc)(int, int); // 为函数指针定义一个别名 MathFunc
  5. 数组类型的 typedef

    #include <stdio.h>
    
    typedef int IntArray[10];
    
    int main() {
        IntArray arr = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
    
        // 输出数组元素
        for (int i = 0; i < 10; i++) {
            printf("%d ", arr[i]);
        }
        printf("\n");
    
        return 0;
    }

#include <stdio.h>

#define INT32 int*

typedef int int32;

int main()
{

    unsigned INT32 a;

    unsigned int32 b;//err 

    return 0;
}

存储类型的关键字不能同时出现

        在 C 语言中,存储类关键字描述了变量的存储方式和生命周期。常见的存储类关键字包括 autoregisterstaticextern 和 typedef。在 C 语言中,一个变量的声明通常只能使用一个存储类关键字,而不能同时使用多个存储类关键字。


#include <stdio.h>

#define INT32 int*

typedef static int int32;//错误,同时使用了 static 和 int

int main()
{
    unsigned INT32 a;

    auto static int x; // 错误,同时使用了 auto 和 static
 
    return 0;
}

注释,\:续行符  转义字符


#include <stdio.h>

# define a "aa" "bb" "cc"

int main()
{

    printf("%s\n",a);//aabbcc
    int/* */i;
    char* s = "abcdef       //higk";
    //is it a\
     ssss     \续行符后不能有任何字符,包括空格。 也可以续航注释
    printf(s);
    in/**/t k;//错误注释被替换,本质是替换为空格
    /*/**/*/  不能嵌套 
    return 0;
}

回车与换行

        在计算机领域,"回车"(Carriage Return)和"换行"(Line Feed)是两个与文本处理和显示有关的概念。

回车(Carriage Return):

  • 在计算机中的表示: 回车通常用 ASCII 字符 \r 来表示。
  • 含义: 回车符最初来自于打字机时代,当打字头到达行的末尾时,打字头需要回到行首,这个操作就是回车。在计算机中,回车表示光标移动到当前行的开头位置。
  • 作用: 在某些系统中,回车符用于表示光标返回到当前行的开头,但不换行,通常与换行符一起使用。

换行(Line Feed):

  • 在计算机中的表示: 换行通常用 ASCII 字符 \n 来表示。
  • 含义: 换行符表示将光标移动到下一行的开头位置,即使在不同操作系统中,换行符的意义也是一致的。
  • 作用: 换行符用于结束当前行,并将光标移至下一行的开头,实现文本在屏幕或其他输出设备上的逐行显示。

在 C 语言中的使用:

        在 C 语言中,'\r' 表示回车,'\n' 表示换行。通常,回车和换行符一起使用来表示一个完整的新行,即回车换行(CRLF)组合。在不同操作系统中,对于文本文件的行尾标识是不同的:在 Windows 中是 CRLF(\r\n),在 Unix/Linux 中是 LF(\n)。

#include <stdio.h>
#include <windows.h>

int main() {

    printf("Loading: ["); // 不换行

    for (int i = 0; i <= 100; i += 10) 
    {
      
        printf("\rLoading: [%d%%]", i); // 回到行首更新加载进度
        fflush(stdout); // 刷新输出缓冲区
        Sleep(1000);

    }

    printf("\nLoading complete!\n");

    return 0;
}
#define _CRT_SECURE_NO_WARNINGS

#include <stdio.h>
#include <windows.h>

int main() {

    int index = 0;
    const char* lable = "|/-\\";

    while (1)
    {
        index %= 4;//确保 index 的值始终在一个特定的范围内,比如在本例中,index 在每次循环之前都会被限制在 0 到 3 之间(因为取余运算的结果始终在 0 和除数之间)。

        printf("\r[%c]", lable[index++]);
      
        Sleep(100);
    }

    return 0;
}

大小端

        大小端(Endianness)是指存储多字节数据时的字节序规则。在计算机系统中,数据可以以两种方式存储:大端字节序和小端字节序。

  • 大端字节序(Big Endian):在大端字节序中,数据的高字节存储在低地址,而低字节存储在高地址。例如,十六进制数0x12345678在大端字节序中存储为12 34 56 78

  • 小端字节序(Little Endian):在小端字节序中,数据的低字节存储在低地址,而高字节存储在高地址。使用相同的例子,十六进制数0x12345678在小端字节序中存储为78 56 34 12

在实际中,不同的系统可能采用不同的字节序,因此在处理二进制数据时需要考虑字节序的影响。

浮点数的二进制表示

浮点数的二进制表示遵循IEEE 754标准,这个标准定义了单精度(32位)和双精度(64位)浮点数的表示方法。下面分别介绍单精度和双精度浮点数的二进制表示:

单精度浮点数(32位)

单精度浮点数由32位二进制数表示,按照IEEE 754标准包括三部分:

  1. 符号位(1位):指示正负号,0表示正数,1表示负数。
  2. 指数位(8位):用于表示指数部分。
  3. 尾数位(23位):用于表示小数部分的尾数。

32位单精度浮点数的二进制表示形式如下:

SEEEEEEEE EMMMMMMM MMMMMMMM MMMMMMMM
  • S:符号位(1位)
  • E:指数位(8位)
  • M:尾数位(23位)

双精度浮点数(64位)

双精度浮点数由64位二进制数表示,也包括符号位、指数位和尾数位,但长度分配不同。

64位双精度浮点数的二进制表示形式如下:

SEEEEEEE EEEEMMMM MMMMMMMM MMMMMMMM MMMMMMMM MMMMMMMM MMMMMMMM MMMMMMMM
  • S:符号位(1位)
  • E:指数位(11位)
  • M:尾数位(52位)

要将实际的浮点数转换为其二进制表示,需要进行一系列的步骤,包括将整数部分和小数部分分开,然后分别转换为二进制表示,最后组合起来形成浮点数的二进制表示。

任意一个二进制浮点数可以表示如下:

(-1)^S*M*2^E 

(-1)^S表示符号位,当S=0时为正数,当S=1时为负数。

M表示有效数字,大于等于1,小于2。

2^E表示指数位。

例如:
十进制的5.0,二进制为101.0表示为(-1)^0*1.01 * 2^2。

其S=0,M=1.01,E=2;

        根据IEEE 754标准,对于单精度浮点数,尾数部分M的整数位总是1,因此可以在存储M时省略这个整数位1,只存储尾数的小数部分。这样就可以用23个二进制位来存储24位有效数字,从而减少存储空间的使用。

在单精度浮点数中,尾数部分M的存储格式如下:

  • 尾数M = 1.xxxxxxx... (小数部分)

其中整数部分的1是隐含的,因此在实际存储时,只需要存储尾数的小数部分,而不需要额外存储整数位1。

这种设计方式有效地提高了浮点数的存储效率,因为可以利用这种隐含的1,减少了需要存储的位数,从而节省了存储空间。

需要注意的是,在进行浮点数计算时,需要根据IEEE 754标准的规定将隐含的整数位1加回,以便正确地进行运算。浮点数的计算需要考虑到这种隐含位,以避免精度丢失或错误的计算结果。

        在浮点数表示中,偏移是指在存储浮点数时对指数部分进行调整,以便能够表示正负指数。在IEEE 754浮点数标准中,偏移是通过在指数字段中使用一个固定的偏移量来实现的。

单精度浮点数的偏移量:

  • 对于单精度浮点数(32位),偏移量是127。这意味着,实际的指数值要加上127才是存储在浮点数中的指数值。

双精度浮点数的偏移量:

  • 对于双精度浮点数(64位),偏移量是1023。和单精度类似,实际的指数值要加上1023才是存储在浮点数中的指数值。

偏移的作用:

  1. 表示负指数:通过偏移,浮点数可以表示负指数,因为指数字段是无符号的,但通过偏移可以表示负指数。

  2. 规范化:偏移也用于规范化浮点数,即确保指数部分在一定范围内,方便对浮点数进行比较以及进行其他操作。

如何进行偏移:

在将浮点数转换为二进制表示时,需要将实际的指数值加上偏移量,然后将结果表示为二进制形式。具体步骤如下:

  1. 计算实际的指数值。
  2. 将实际的指数值加上偏移量。
  3. 将得到的结果转换为二进制形式。

举例:

假设有一个单精度浮点数的指数部分的实际值为3。在偏移量为127的情况下,要将这个指数值加上偏移量进行存储:

实际指数值:3
加上偏移量:3 + 127 = 130
二进制表示:130的二进制形式为10000010

假设要将单精度浮点数-6.25转换为二进制:

  1. 符号位:负数,符号位为1。
  2. 绝对值为6.25,整数部分为6,小数部分为0.25
  3. 整数部分转换为二进制为110,小数部分转换为二进制为01
  4. 规格化为1.1001 * 2^2
  5. 指数部分为2,偏移后为129,转换为二进制为10000001,尾数0.1001
  6. 组合符号位、指数部分和尾数部分:1 10000001 10010000000000000000000.
#include <stdio.h>
#include <stdint.h>

// 定义一个union结构FloatBits,包含一个float类型的变量f和一个uint32_t类型的变量u
union FloatBits {
    float f;
    uint32_t u;
};

// 打印一个无符号整数的二进制表示
void printBinary(uint32_t num) {
    for(int i = 31; i >= 0; i--) {
        printf("%d", (num >> i) & 1);
        if (i % 4 == 0) printf(" ");  // 每4位添加一个空格,以提高可读性
    }
    printf("\n");
}

int main() {
    // 将-6.25赋值给变量n
    float n = -6.25;

    // 创建一个FloatBits类型的联合fb,并将n赋给其中的浮点数部分
    union FloatBits fb;
    fb.f = n;

    // 打印浮点数和其二进制表示
    printf("浮点数: %f\n", n);
    printf("二进制表示: ");
    printBinary(fb.u);

    return 0;
}

多个指针指向同一个常量字符串

#include <stdio.h>

int main()
{
    char* p1 = "abcde";
    char* p2 = "abcde";
    printf("%p\n", p1);
    printf("%p\n", p2);

    // *p1 = 'a';//err 常量值不能改    

    char arr1[] = "abcde";
    char arr2[] = "abcde";
    printf("%p\n", arr1);
    printf("%p\n", arr2);

    *arr1 = 'b';
    printf("%s\n", arr1);

    return 0;
}

        在C语言中,字符串常量存储在只读存储区域,因此多个指向相同字符串常量的指针将指向相同的内存位置。而字符数组在栈上存储,每次声明都会分配新的内存空间。

以下是代码的输出解释:

  1. p1 和 p2 是指向字符串常量 "abcde" 的两个指针。由于它们指向相同的字符串常量,因此它们的值相同。

    0x地址1
    0x地址1
    
  2. arr1 和 arr2 是两个包含相同内容的字符数组。每个字符数组都有自己的内存位置,因此它们在不同的内存地址上。

    0x地址2
    0x地址3
    

这种行为是由C语言中字符串常量和字符数组的存储方式引起的。字符串常量是不可变的,存储在只读内存区域,而字符数组是可变的,并在栈上分配内存。

数组指针与指针数组

        在C语言中,数组指针(Array Pointer)和指针数组(Pointer Array)是两个重要的概念,它们虽然在名称上很相似,但在用法和含义上有一些本质的区别。

数组指针(Array Pointer):

  • 定义:数组指针是指向数组的指针,它指向数组的首地址。
  • 声明方式:使用括号来明确指针指向的是数组,而非单个元素。
  • 用途:通常用于遍历整个数组或作为函数参数传递整个数组。

示例:

int arr[5] = {1, 2, 3, 4, 5};
int (*ptr)[5]; // 声明一个指向包含5个整数的数组的指针
ptr = &arr; // 将数组arr的首地址赋给ptr

指针数组(Pointer Array):

  • 定义:指针数组是一个数组,其中的元素都是指针(地址),每个指针可以指向不同的内存位置。
  • 声明方式:声明一个数组,每个元素都是指针。
  • 用途:通常用于存储不同类型或数量的指针。

示例:

int num1 = 10, num2 = 20, num3 = 30;
int *ptrArray[3]; // 声明一个包含3个整型指针的指针数组
ptrArray[0] = &num1; // 第一个指针指向num1
ptrArray[1] = &num2; // 第二个指针指向num2
ptrArray[2] = &num3; // 第三个指针指向num3

区别总结:

  • 数组指针是指向数组的指针,可以用来访问整个数组的内容。
  • 指针数组是一个数组,其中的元素都是指针,每个指针可以指向不同的内存位置。

数组名通常表示的是数组首元素的地址,但sizeof(数组名)计算的是整个数组的大小。

&数组名也是表示整个数组的地址。

#include <stdio.h>

int main() 
{
    int arr[10] = { 0 };

    printf("%zu\n", sizeof(arr));//40

    printf("%p\n", arr);
    printf("%p\n", arr + 1);//相差4字节

    printf("%p\n", &arr[0]);
    printf("%p\n", &arr[0] + 1);//相差4字节

    printf("%p\n", &arr);
    printf("%p\n", &arr + 1);//相差40字节

    return 0;
}

函数指针与指针函数

指针函数和函数指针是两个在C语言中经常混淆的概念。

  1. 指针函数:指针函数是一个返回指针的函数。这意味着函数的返回类型是一个指针,指向特定类型的数据。例如,一个返回整数指针的函数可以被称为指针函数。
int* pointerFunction() {
    int x = 10;
    int *ptr = &x;
    return ptr;
}
  1. 函数指针:函数指针是指向函数的指针变量。通过函数指针,可以在运行时动态地选择调用哪个函数。函数指针的声明类似于函数原型,只是在函数名前面加上了*
int add(int a, int b) {
    return a + b;
}

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

int (*functionPtr)(int, int); // 函数指针的声明

functionPtr = &add; // 将函数指针指向add函数
int result = functionPtr(10, 5); // 调用add函数

functionPtr = &subtract; // 将函数指针指向subtract函数
result = functionPtr(10, 5); // 调用subtract函数

总结来说,指针函数是返回指针类型的函数,而函数指针是指向函数的指针变量,用于间接调用函数。

#include <stdio.h>

void show(int x)
{
    printf("%d\n",x);
}

int main() 
{
    // (*(void(*)())0)();  将空指针 0 强制转换为一个函数指针,然后解引用该函数指针并调用它
    typedef void(*pf_t)(int);
    void (*signal(int, void(*)(int)))(int);
    pf_t signal(int, pf_t);

    void (*prin)(int);
    prin = show;
    prin(5);

    pf_t p;
    p = show;
    p(6);

    return 0;
}

数组的类型

数组也是有类型的,如:int arr[10]数组arr的类型就是int[10]。

函数指针数组与转移表

        函数指针数组在C语言中是一个数组,其中的每个元素都是一个函数指针。这种结构非常有用,可以用于实现函数的动态调用,根据需要选择调用不同的函数。

以下示例,展示了如何声明和使用函数指针数组:

#include <stdio.h>

void function1() {
    printf("This is function 1\n");
}

void function2() {
    printf("This is function 2\n");
}

void function3() {
    printf("This is function 3\n");
}

int main() {
    // 声明一个函数指针数组,包含指向三个不同函数的指针
    void (*function_pointers[3])() = {function1, function2, function3};

    // 使用循环调用函数指针数组中的函数
    for (int i = 0; i < 3; i++) {
        function_pointers[i]();
    }

    return 0;
}

在这个示例中,定义了三个简单的函数 function1function2 和 function3,它们分别打印不同的消息。然后,声明了一个包含三个函数指针的函数指针数组 function_pointers,每个指针指向一个不同的函数。

通过循环遍历函数指针数组,并调用每个函数指针,实现了动态调用不同函数的效果。

函数指针数组在实际编程中常用于实现状态机、回调函数等功能,具有很高的灵活性和可扩展性。

        在计算机编程中,"转移表"(Jump Table)通常是指使用函数指针数组来实现的一种技术。转移表是一种用于根据输入值跳转到不同函数或代码块的方法。

下面是一个简单的示例,展示了如何使用转移表实现一个简单的计算器程序:

#include <stdio.h>

// 函数原型声明
void add(int a, int b);
void subtract(int a, int b);
void multiply(int a, int b);
void divide(int a, int b);

int main() {
    void (*function_table[])(int, int) = {add, subtract, multiply, divide};

    int choice, num1, num2;

    printf("Enter two numbers: ");
    scanf("%d %d", &num1, &num2);

    printf("Choose an operation:\n");
    printf("1. Add\n");
    printf("2. Subtract\n");
    printf("3. Multiply\n");
    printf("4. Divide\n");
    scanf("%d", &choice);

    if (choice >= 1 && choice <= 4) {
        function_table[choice - 1](num1, num2);
    } else {
        printf("Invalid choice\n");
    }

    return 0;
}

void add(int a, int b) {
    printf("Result of addition: %d\n", a + b);
}

void subtract(int a, int b) {
    printf("Result of subtraction: %d\n", a - b);
}

void multiply(int a, int b) {
    printf("Result of multiplication: %d\n", a * b);
}

void divide(int a, int b) {
    if (b != 0) {
        printf("Result of division: %d\n", a / b);
    } else {
        printf("Error: Division by zero\n");
    }
}

在这个示例中,使用函数指针数组 function_table 来实现一个简单的计算器程序。用户可以输入两个数字以及选择一个操作(加法、减法、乘法、除法),然后根据用户的选择,通过转移表来调用相应的函数进行计算。

这种方法可以使代码更加模块化和灵活,通过简单地修改函数指针数组,可以轻松扩展或更改支持的操作。

回调函数

        回调函数(Callback Function)是一种常见的编程概念,特别在C语言中经常被使用。回调函数是指在某些特定事件发生时由另一个函数调用的函数,允许程序在运行时动态地注册和调用函数。

下面是一个简单的示例,展示了如何使用回调函数:

#include <stdio.h>

// 回调函数原型
typedef void (*callback_function)(int);

// 函数原型
void perform_operation(int value, callback_function callback);

// 回调函数1
void callback1(int value) {
    printf("Callback 1: %d\n", value);
}

// 回调函数2
void callback2(int value) {
    printf("Callback 2: %d\n", value * 2);
}

int main() {
    int data = 10;

    // 调用 perform_operation 函数并注册不同的回调函数
    perform_operation(data, callback1);
    perform_operation(data, callback2);

    return 0;
}

// 执行操作并调用回调函数
void perform_operation(int value, callback_function callback) {
    printf("Performing operation with value: %d\n", value);
    callback(value);
}

        在这个示例中,首先定义了两个回调函数 callback1 和 callback2,它们分别用于在 perform_operation 函数中进行回调。

perform_operation 函数接受一个整数值和一个回调函数作为参数,它执行一个操作并在操作完成后调用传入的回调函数。

通过回调函数,可以实现更加灵活和可扩展的代码结构,允许在运行时动态地传递和调用不同的函数。

函数指针数组指针

        在C语言中,函数指针数组指针是指一个指向函数指针数组的指针。这种结构可以用于动态管理函数指针数组,允许在运行时对其进行修改或选择不同的函数指针数组。

下面是一个示例,展示了如何声明和使用函数指针数组指针:

#include <stdio.h>

void function1() {
    printf("This is function 1\n");
}

void function2() {
    printf("This is function 2\n");
}

void function3() {
    printf("This is function 3\n");
}

int main() {
    // 声明一个函数指针数组
    void (*function_pointers[3])() = {function1, function2, function3};

    // 声明一个指向函数指针数组的指针
    void (**function_pointer_ptr)() = function_pointers;

    // 通过指针间接调用函数指针数组中的函数
    for (int i = 0; i < 3; i++) {
        (*function_pointer_ptr[i])();
    }

    return 0;
}

        在这个示例中,先定义了三个简单的函数 function1function2 和 function3,它们分别打印不同的消息。然后,声明了一个包含三个函数指针的函数指针数组 function_pointers

接着,声明了一个指向函数指针数组的指针 function_pointer_ptr,并将其指向 function_pointers。通过这个指针,可以间接访问和调用函数指针数组中的函数。

通过函数指针数组指针,可以更灵活地管理函数指针数组,使得在运行时可以动态地选择不同的函数指针数组或对其进行修改。

strcpy

    strcpy 是一个C标准库函数,用于将一个字符串(以\0结尾)复制到另一个字符串中,包括字符串的结尾符号\0。在使用 strcpy 函数时需要注意目标字符串的大小,以避免发生缓冲区溢出的情况。

下面是 strcpy 函数的基本用法示例:

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

int main() {
    char source[] = "Hello, World!";
    char destination[20]; // 目标字符串大小需要足够容纳源字符串及其结尾符号

    // 使用 strcpy 函数将 source 字符串复制到 destination 字符串
    strcpy(destination, source);

    // 输出复制后的字符串
    printf("Source string: %s\n", source);
    printf("Destination string: %s\n", destination);

    return 0;
}

        需要注意的是,strcpy 函数是一个基本的字符串操作函数,但由于它不检查目标数组的大小,因此容易导致缓冲区溢出的问题。因此,在实际开发中,应该优先考虑使用更安全的函数,比如 strncpy 等,或者确保目标字符串足够大,以避免出现潜在的安全风险。

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

int main()
{
	char name[7] = "李四";
	//name = "张三";//err name数组名是一个地址,地址是常量值,不能被赋值。

	printf("%s\n", name);
	strcpy(name, "张三");
	printf("%s\n", name);
	return 0;
}
char* strcpy(char* destination, const char* source);

strncpy 

   strncpy 是 C 语言中一个字符串操作函数,用于将一个字符串的一部分拷贝到另一个字符串中。这个函数的原型如下:

char *strncpy(char *destination, const char *source, size_t num);
  • destination:目标字符串,接收拷贝内容的字符串数组。
  • source:源字符串,要复制内容的字符串。
  • num:要复制的字符数,即最大允许复制的字符数。

strncpy 函数会将源字符串 source 中的最多 num 个字符(字符数不包括字符串结尾的 NULL 字符 \0)拷贝到目标字符串 destination 中。如果 source 的长度小于 num,则会在目标字符串中用 NULL 字符 \0 填充剩余的空间。

下面是一个简单的示例,演示了如何使用 strncpy 函数:

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

int main() {
    char source[] = "Hello, World!";
    char destination[20]; // 目标字符串数组

    // 使用 strncpy 将 source 的前 5 个字符拷贝到 destination 中
    strncpy(destination, source, 5);
    destination[5] = '\0'; // 手动添加字符串结束符

    printf("Copied string: %s\n", destination);

    return 0;
}

在这个示例中,strncpy(destination, source, 5); 将源字符串 source 的前 5 个字符拷贝到目标字符串 destination 中,然后手动添加字符串结束符 \0

strcat

    strcat 是一个C标准库函数,用于将一个字符串(以\0结尾)追加到另一个字符串的末尾,并确保新字符串以\0结尾。与 strcpy 不同,strcat 不会覆盖目标字符串的结尾符号,而是将新字符串直接追加到目标字符串的结尾。

下面是一个简单的示例展示了 strcat 函数的基本用法:

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

int main() {
    char str1[20] = "Hello, ";
    char str2[] = "World!";

    // 使用 strcat 将 str2 追加到 str1 的末尾
    strcat(str1, str2);

    // 输出追加后的字符串
    printf("Concatenated string: %s\n", str1);

    return 0;
}

与 strcpy 一样,使用 strcat 时需要确保目标字符串足够大,以避免发生缓冲区溢出的问题。如果目标字符串的空间不足以容纳要追加的内容,可能会导致未定义的行为。

strncat

    strncat 是 C 语言中用于将一个字符串的一部分连接(追加)到另一个字符串的函数。这个函数的原型如下:

char *strncat(char *destination, const char *source, size_t num);
  • destination:目标字符串,接收连接内容的字符串数组。
  • source:源字符串,要追加内容的字符串。
  • num:要追加的字符数,即最大允许追加的字符数。

strncat 函数会将源字符串 source 中的最多 num 个字符(字符数不包括字符串结尾的 NULL 字符 \0)追加到目标字符串 destination 的末尾,并在连接后的字符串末尾添加 NULL 字符 \0

下面是一个简单的示例,演示了如何使用 strncat 函数:

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

int main() {
    char destination[20] = "Hello, ";
    char source[] = "World!";

    // 使用 strncat 将 source 的前 3 个字符追加到 destination 的末尾
    strncat(destination, source, 3);

    printf("Concatenated string: %s\n", destination);

    return 0;
}

在这个示例中,strncat(destination, source, 3); 将源字符串 source 的前 3 个字符追加到目标字符串 destination 的末尾。

char* strcat(char* destination,const char* source);

strtok

    strtok 是一个C标准库函数,用于将字符串分割成多个子字符串。它会根据指定的分隔符将输入字符串切割成多个部分,并返回一个指向当前部分的指针。在每次调用 strtok 时,会返回一个新的部分,直到整个字符串被分割完毕。

下面是一个简单的示例展示了 strtok 函数的基本用法:

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

int main() {
    char str[] = "apple,orange,banana,grapes";
    char *token;

    // 使用 strtok 函数分割字符串并输出各部分
    token = strtok(str, ",");
    while (token != NULL) {
        printf("%s\n", token);
        token = strtok(NULL, ",");
    }

    return 0;
}

在这个示例中,我们定义了一个字符串 str,其中包含了多个水果名称,每个水果名称之间使用逗号作为分隔符。

使用 strtok 函数首先将 str 字符串以逗号为分隔符进行分割,然后在一个循环中逐个输出各个部分。在第一次调用 strtok 时,需要传入要分割的字符串以及分隔符。在后续的调用中,将第一个参数设为 NULL,以便继续从上一次分割的位置继续分割字符串。

输出结果将会是每个水果名称单独一行的形式,即:

apple
orange
banana
grapes

需要注意的是,strtok 函数会修改输入字符串,将分隔符替换为\0,因此在使用 strtok 时要注意保存原始字符串或者在复制一份副本上进行操作,以避免意外修改原始字符串的内容。

char *strtok(char *str, const char *delim);

strerror

    strerror 是一个C标准库函数,用于将错误码转换为相应的错误消息字符串。通常情况下,系统调用或库函数在失败时会设置全局变量 errno 来指示错误代码,strerror 函数可以将 errno 的值转换为相应的错误消息。

以下是一个简单的示例展示了 strerror 函数的基本用法:

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

int main() {
    FILE *file = fopen("non_existent_file.txt", "r");

    if (file == NULL) {
        perror("Error opening file");
        printf("Error message: %s\n", strerror(errno));
    } else {
        fclose(file);
    }

    return 0;
}

在这个示例中,我们尝试以只读方式打开一个不存在的文件。如果打开文件失败,会输出一个自定义的错误消息,并通过 strerror(errno) 将 errno 转换为具体的错误消息打印出来。

需要注意的是,strerror 函数通常用于调试和错误处理,以便开发人员可以更好地了解程序中发生的错误。

#include <errno.h>
char* strerror(int errnum);

传指针数组,二维数组

        指针数组是一个数组,其中的每个元素都是指针。当你需要将指针数组传递给函数或者进行操作时,可以使用二级指针(指向指针的指针)来接收指针数组。除了使用二级指针外,还有其他方法来处理指针数组,例如使用数组名本身或者使用指针来接收指针数组。

以下是一些常见的方式来处理指针数组:

  1. 使用二级指针:通过使用二级指针,可以接收指针数组,对其进行修改或者操作
#include <stdio.h>

void processPtrArray(int** ptrArray, int size) {
    for (int i = 0; i < size; i++) {
      
        printf("%s ", ptrArray[i]);
    }
}

int main() {
    char arr1[6] = "hello";
    char arr2[5] = "word";
    char arr3[2] = "!";

    int* ptrArray[3] = { arr1,arr2,arr3 };  // 指针数组
    // 使用二级指针接收指针数组
    processPtrArray(ptrArray, 3);

    return 0;
}
  1. 使用数组名本身:在函数参数中,可以直接将数组名作为指针传递给函数,函数参数中使用指针来接收。这种方法在传递指针数组时很常见。
#include <stdio.h>

void processPtrArray(int *ptrArray[], int size) {
    for (int i = 0; i < size; i++) {
      
        printf("%s ", ptrArray[i]);
    }
}

int main() {
    char arr1[6] = "hello";
    char arr2[5] = "word";
    char arr3[2] = "!";

    int* ptrArray[3] = { arr1,arr2,arr3 };  // 指针数组
    // 使用本身数组名收指针数组
    processPtrArray(ptrArray, 3);

    return 0;
}
  1. 使用指针:可以使用指针来接收指针数组的首个元素,然后通过指针算术运算来访问数组中的其他元素。
#include <stdio.h>

void processPtrArray(int **ptrArray, int size) {
    for (int i = 0; i < size; i++) {
      
        printf("%s ", ptrArray[i]);
    }
}

int main() {
    char arr1[6] = "hello";
    char arr2[5] = "word";
    char arr3[2] = "!";

    int* ptrArray[3] = { arr1,arr2,arr3 };  // 指针数组
    int** ptr = ptrArray;  // 使用指针接收指针数组的首个元素
    processPtrArray(ptr, 3);

    return 0;
}

        二维数组是一个数组的数组,每个元素都是一维数组。在C语言中,二维数组的每一行在内存中是连续存储的。要传递二维数组给函数,可以声明函数参数为指向数组的指针,或者使用指向数组首元素的指针。

#include <stdio.h>

void process2DArray(int arr[][3], int rows, int cols) {
    for (int i = 0; i < rows; i++) {
        for (int j = 0; j < cols; j++) {
            printf("%d", arr[i][j]);
        }
        printf("\n");
    }
}

int main() {
    int arr[2][3] = { {1, 2, 3}, {4, 5, 6} };  // 二维数组
    // 将二维数组传递给函数
    process2DArray(arr, 2, 3);

    return 0;
}

 使用指向数组的指针传递二维数组

#include <stdio.h>

void process2DArray(int (*arr)[3], int rows, int cols) {
    for (int i = 0; i < rows; i++) {
        for (int j = 0; j < cols; j++) {
            printf("%d ", arr[i][j]);
        }
        printf("\n");
    }
}

int main() {
    int arr[2][3] = {{1, 2, 3}, {4, 5, 6}};  // 二维数组
    // 将二维数组传递给函数
    process2DArray(arr, 2, 3);

    return 0;
}

使用指向指针的指针传递二维数组

#include <stdio.h>

void process2DArray(int **arr, int rows, int cols) {
    for (int i = 0; i < rows; i++) {
        for (int j = 0; j < cols; j++) {
            printf("%d ", arr[i][j]);
        }
        printf("\n");
    }
}

int main() {
    int arr[2][3] = {{1, 2, 3}, {4, 5, 6}};  // 二维数组
    int *ptrArr[2] = {arr[0], arr[1]};  // 指针数组
    // 将指针数组传递给函数
    process2DArray(ptrArr, 2, 3);

    return 0;
}

使用一维指针传递二维数组

#include <stdio.h>

void process2DArray(int *arr, int rows, int cols) {
    for (int i = 0; i < rows; i++) {
        for (int j = 0; j < cols; j++) {
            printf("%d ", *(arr + i * cols + j));
        }
        printf("\n");
    }
}

int main() {
    int arr[2][3] = {{1, 2, 3}, {4, 5, 6}};  // 二维数组
    // 将二维数组转换为一维指针并传递给函数
    process2DArray(&arr[0][0], 2, 3);

    return 0;
}

内存对齐

        内存对齐是指数据在内存中存储时按照一定规则排列的过程。内存对齐的原因主要是为了提高访问内存的效率,因为许多计算机体系结构要求特定类型的数据在特定地址上对齐,这样可以提高数据读取速度。以下是一些关于内存对齐的常见概念:

1. 数据类型的对齐要求

  • 基本数据类型:不同的数据类型有不同的对齐要求。例如,大多数平台要求 char 类型的数据对齐为1字节,int 类型通常对齐为4字节或8字节,double 类型通常对齐为8字节。

  • 结构体对齐:结构体中的成员变量会根据其自身的对齐要求而进行对齐,同时结构体本身也有可能需要对齐。

2. 内存对齐规则

  • 自然对齐:数据类型的起始地址必须是其自身长度的整数倍。例如,int 类型通常需要4字节对齐,double 类型通常需要8字节对齐。

  • 最严格对齐原则:在结构体中,结构体的大小必须是其最宽基本类型成员大小的整数倍。这样可以确保结构体的每个成员都按照其自身的对齐要求进行对齐,同时结构体本身也按照最宽成员的对齐要求进行对齐。

3. 编译器指令和优化

  • #pragma pack 指令:在C/C++中,#pragma pack 指令可以用来指定结构体成员的对齐方式,例如取消默认的对齐或者设置指定的对齐字节数。

  • 优化:编译器通常会根据平台的要求和优化目标来进行内存对齐。对于一些特殊需求,可以使用编译器提供的指令来调整对齐方式。

4. 结构体的内存对齐示例

#include <stdio.h>

struct Data {
    char a;//1
    int b;//3 + 4
    double c;//8
};

int main() {
    struct Data data;
    printf("Size of struct Data: %lu bytes\n", sizeof(struct Data));
    printf("Address of a: %p\n", &data.a);
    printf("Address of b: %p\n", &data.b);
    printf("Address of c: %p\n", &data.c);

    return 0;
}

在上面的例子中,结构体 Data 中的成员变量会根据对齐规则进行排列,通常会有一些填充字节以满足对齐要求。

位段

        位段(Bit Fields)是C语言提供的一种特性,允许程序员定义结构体成员占用的位数,而不是整个字节。这可以用于节省内存空间,特别是在对内存空间有严格要求的嵌入式系统中。下面是关于位段的一些重要信息:

1. 位段的定义

struct BitField {
    unsigned int a : 4;     // a占4位
    unsigned int b : 8;     // b占8位
    unsigned int c : 2;     // c占2位
    unsigned int : 0;       // 占位,用于字节对齐
    unsigned int d : 10;    // d占10位
};

2. 位段的特点

  • 位段成员:每个位段成员都有一个冒号后面跟着的数字,指定该成员占用的位数。

  • 无名位段:上面示例中的 : 0 是一个无名位段,用于实现字节对齐。

  • 符号位段:位段可以是有符号或无符号的。

3. 位段的注意事项

  • 位段长度:位段的长度不能超过基本数据类型的长度,例如 int 通常是4字节,所以位段最多占32位(4字节)。

  • 位段顺序:位段在内存中的存储顺序取决于具体的编译器及目标平台的实现。

  • 位段操作:位段的操作和普通成员变量类似,但需要注意位段成员的位数。

4. 使用位段的示例

#include <stdio.h>

struct BitField {
    unsigned int a : 4;
    unsigned int b : 8;
    unsigned int c : 2;
};

int main() {
    struct BitField bf;
    bf.a = 2;
    bf.b = 10;
    bf.c = 3;

    printf("Size of struct BitField: %lu bytes\n", sizeof(struct BitField));
    printf("Value of a: %u\n", bf.a);
    printf("Value of b: %u\n", bf.b);
    printf("Value of c: %u\n", bf.c);

    return 0;
}

在上面的例子中,结构体 BitField 中的 a 占4位,b 占8位,c 占2位。通过位段,可以精确控制结构体成员变量占用的位数,节省内存空间。

请注意,位段的使用应该谨慎,因为其行为受编译器实现和目标平台的影响,可能会导致不可移植的代码。

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

void show(unsigned x)
{
    int n = 8;
    while (n--)
    {
        printf("%d ", (x >> n) & 1);
    }
    printf("\n");
}

int main() 
{
    unsigned char puc[4];

    struct tag
    {
        unsigned char ucp1;
        unsigned char uc0 : 1;
        unsigned char uc1 : 2;
        unsigned char uc2 : 3;
    }*pst;

    printf("结构体大小:%zu字节\n", sizeof(struct tag));
    pst = (struct tag*)puc;
    memset(puc, 0, 4);

    pst->ucp1 = 2;//0010  0000 0010 
    pst->uc0 = 3; //0011  截取1
    pst->uc1 = 4; //0100  截取00
    pst->uc2 = 5; //0101  截取101   组合101001补全00101001
    printf("%02x %02x %02x %02x\n", puc[0], puc[1], puc[2], puc[3]);
    show(puc[0]);
    show(puc[1]);
    show(puc[2]);
    show(puc[3]);

    return 0;
}

return;

return;语句用于提前结束函数的执行并返回到调用函数。在函数声明为void的情况下用于提前退出函数,return;语句用于显式的结束函数的执行,即使函数本身不返回任何值。

#include <stdio.h>

int show()
{
    return 9;
}

void show1()
{
    show();
    return ;
}

int my_func()
{
    show1();
    return;
}

int main() 
{
  
    int ret = my_func();
    printf("Result: %d\n", show());
    printf("Result: %d\n", ret);

    return 0;
}

fprintf

   fprintf 是 C 语言中用于将格式化数据输出到文件流的函数。它的原型如下:

int fprintf(FILE *stream, const char *format, ...);
  • stream:指向文件流的指针,确定数据写入的位置。
  • format:格式化字符串,包含要写入的数据以及格式规范。
  • ...:可变数量的参数,根据 format 字符串中的格式规范提供要写入的数据。

fprintf 函数的工作原理与 printf 函数类似,但 fprintf 将输出结果写入到指定文件流中,而不是标准输出(屏幕)。

fscanf

   fscanf 是 C 语言中用于从文件流中读取格式化输入的函数。它的原型如下:

int fscanf(FILE *stream, const char *format, ...);
  • stream:指向文件流的指针,确定从哪个文件读取数据。
  • format:格式化字符串,指定了如何解析文件中的数据。
  • ...:可变数量的参数,根据 format 字符串中的格式规范存储读取到的数据。

fscanf 函数的工作方式类似于 scanf 函数,但是 fscanf 从文件流中读取数据,而不是从标准输入(键盘)中读取数据。

FILE *file = fopen("data.txt", "r");
int num;
fscanf(file, "%d", &num);

FILE *output = fopen("output.txt", "w");
fprintf(output, "Number: %d\n", num);

fclose(file);
fclose(output);

  • scanf:从标准输入(键盘)读取格式化输入。
  • printf:向标准输出(屏幕)写入格式化输出。
  • sscanf:从字符串中读取格式化输入。
  • sprintf:将格式化输出写入字符串。
  • fscanf:从文件流中读取格式化输入。
  • fprintf:向文件流写入格式化输出。
#include <stdio.h>
#include <string.h>

struct S
{
    char name[7];
    int age;
};

int main() 
{
  
    struct S s = { "张三",20 };
    struct S tmp = { 0 };
    char buf[100] = { 0 };

    sprintf(buf, "%s %d", s.name, s.age);
    printf("%s\n", buf);

    sscanf(buf, "%s %d", tmp.name, &(tmp.age));
    printf("%s %d\n", tmp.name, tmp.age);

    return 0;
}

宏定义

        在 C 语言中,宏定义是一种预处理指令,用于在编译程序之前替换文本。通过宏定义,可以为常量、函数、代码段等创建符号名称,从而提高代码的可读性、灵活性和重用性。

字符串宏常量

        在C语言中,可以使用字符串宏常量来定义路径。这种方法可以让你在程序中引用路径时更具可读性和可维护性。

#include <stdio.h>

#define PATH_TO_FILE "C:\\Users\\Username\\Documents\\file.txt"

int main() {
    printf("Path to file: %s\n", PATH_TO_FILE);
    return 0;
}

下面是一些关于宏定义的常见用法:

  1. 定义常量:使用 #define 关键字可以定义常量。

    #define PI 3.14159
    
  2. 带参数的宏:可以定义带参数的宏,类似于函数。

    #define SQUARE(x) ((x) * (x))
    
  3. 条件编译:使用宏来进行条件编译,根据条件选择性地包含代码。

    #define DEBUG
    
    #ifdef DEBUG
        // 调试代码
    #endif
    
  4. 字符串化:使用 # 运算符将参数转换为字符串。

    #define STRINGIFY(x) #x
    
  5. 连接运算符:使用 ## 运算符连接两个标记。

    #define CONCAT(x, y) x ## y
    
  6. 条件表达式:使用 #if 和 #else 定义条件表达式。

    #if SIZE == 1
        // 代码块
    #else
        // 另一段代码块
    #endif
#include <stdio.h>

#define CONCAT(x, y) x ## y

int main() {
    int num = 10;
    int CONCAT(var, num) = 20; // 在这里,CONCAT(var, num) 将被替换为 varnum

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

    return 0;
}
#define name(parament_list) stuff

//parament_list是由逗号隔开的符号表

//参数列表的左括号必须与name紧邻,若两者存在空格,参数就会被解释为stuff的一部分

#include <stdio.h>

#define PRINT(f,v) printf("the value of "#v" is "f" \n",v);
#define LINK(a,b) printf("the words is "#a###b);

int main() 
{
    int i = 1;
    PRINT("%d", i + 6)
    LINK(aa, bbb)
    return 0;
}

文件名不能包含的字符

        在大多数现代操作系统中,文件名通常受到一些限制,不能包含特定的字符或字符序列。以下是一般情况下文件名中不能包含的常见字符:

  1. Windows 操作系统

    • \ / : * ? " < > |
  2. Unix/Linux 操作系统

    • /(正斜杠):用作路径分隔符。
    • \0(空字符):表示字符串结束。
    • :(冒号):在某些上下文中被用作特殊用途。
    • *(星号):在通配符中用作通配符。
    • ?(问号):在通配符中用作通配符。
    • "(双引号):在某些情况下用作特殊字符。
    • <(小于号):在某些情况下用作特殊字符。
    • >(大于号):在某些情况下用作特殊字符。
    • |(竖线):在某些情况下用作特殊字符。
  3. 通用建议

    • 最好避免使用空格或特殊字符作为文件名,因为它们可能会引起一些问题,尤其在命令行中处理文件时。
    • 最好使用字母、数字、下划线和连字符等常见字符来构建文件名。

确保文件命名符合操作系统的规定可以避免出现文件操作中的问题。

%n

  %n 是一个格式化字符串,通常在C语言的 printf 函数中使用。在 printf 中,%n 会将已经打印的字符数(即输出的字符数量)写入到参数对应的整型变量中。这个格式化字符被vs禁用了。

用这个格式化字符会断言失败。但在一些老的编译器(vc6.0)可以使用。

几种取整方式

  1. 向下取整(向负无穷取整)

    • 函数/操作floor() 函数
    • 示例double floor(double x);
    • 描述:返回不大于参数的最大整数值。
  2. 向上取整(向正无穷取整)

    • 函数/操作ceil() 函数
    • 示例double ceil(double x);
    • 描述:返回不小于参数的最小整数值。
  3. 四舍五入取整

    • 函数/操作round() 函数
    • 示例double round(double x);
    • 描述:返回参数的最接近整数值,四舍五入。
  4. 向零取整(截断取整):

    • 函数/操作:类型转换操作符
    • 示例(int)x
    • 描述:直接将浮点数转换为整数,舍弃小数部分。
  5. 向最近的偶数取整

    • 函数/操作rint() 函数
    • 示例double rint(double x);
    • 描述:返回最接近参数的整数值,如果有两个数同样接近,则返回偶数。

这些取整方式在不同的场景下有不同的应用,根据需求选择合适的取整方式可以保证数据处理的正确性和精确性。在实际编程中,根据具体情况选择适合的取整方式是非常重要的。

#include <stdio.h>
#include <math.h>

int main() {
    double x = 3.8;
    
    // Floor - 向下取整
    double floor_result = floor(x);
    printf("Floor of %.2f is %.2f\n", x, floor_result);

    // Ceil - 向上取整
    double ceil_result = ceil(x);
    printf("Ceil of %.2f is %.2f\n", x, ceil_result);

    // Round - 四舍五入
    double round_result = round(x);
    printf("Rounded value of %.2f is %.2f\n", x, round_result);

    // Truncate - 向零取整
    int truncated_result = (int)x;
    printf("Truncated value of %.2f is %d\n", x, truncated_result);

    // Rint - 向最近的偶数取整
    double rint_result = rint(x);
    printf("Nearest even integer of %.2f is %.2f\n", x, rint_result);

    return 0;
}
#include <stdio.h>
#include <math.h>

int main() 
{
    const char* format = "%.1f \t%.1f \t%.1f \t%.1f \t%.1f\n";
    printf("value\tround\tfloor\tceil\ttrunc\n");

    float n = 0;
    n = 2.3f;
    printf(format, n, round(n), floor(n), ceil(n), trunc(n));
    n = 3.9f;
    printf(format, n, round(n), floor(n), ceil(n), trunc(n));
    n = 6.5f;
    printf(format, n, round(n), floor(n), ceil(n), trunc(n));
    n = -2.3f;
    printf(format, n, round(n), floor(n), ceil(n), trunc(n));
    n = -3.9f;
    printf(format, n, round(n), floor(n), ceil(n), trunc(n));
    n = -6.5f;
    printf(format, n, round(n), floor(n), ceil(n), trunc(n));

    return 0;
}

        在C语言中,当进行整数除法时,除法运算会向零取整。这意味着结果将舍弃小数部分,保留整数部分。具体来说,C语言中的整数除法规则如下:

  • 向零取整规则:整数除法会向零取整,即直接舍弃小数部分,保留整数部分。这意味着无论操作数的正负性如何,结果将取离零更近的整数部分。
#include <stdio.h>

int main() 
{
   
    printf("%d\n", 10 / -3);//-3
    printf("%d\n", -10 / 3);//-3
    printf("%d\n", -10 / -3);//3
    printf("%d\n", 10 % -3);//1
    printf("%d\n", -10 % 3);//-1
    printf("%d\n", -10 % -3);//-1

    return 0;
}

        有a和d两个自然数,d非零,可以证明存在两个唯一的整数q和r,满足a = q*d + r,q为整数,且0 <= |r| < |d|。其中,q为商,r为余数。

预处理先去掉注释后宏替换

#include <stdio.h>
//先执行去注释 BSC // 就变成了 BSC 宏替换后就是空白printf("hello world");
#define BSC //   
#define BMC /*
#define EMC */

int main() {
    BSC printf("hello world");

    BMC printf("hello world"); //EMC 被注释了err

    return 0;
}
#include <stdio.h>

#define INT_VAL(x,y) x=0;y=0;
int main() 
{
    int a = 10;
    int b = 12;
    
    printf("%d,%d\n", a, b);
    if (1)
        INT_VAL(a, b)//err a=0;b=0;  if在没有大括号且后跟else时只能有一条语句
   //else
        printf("%d,%d\n", a, b);
    return 0;
}

        宏定义中使用do { ... } while (0) 结构来确保宏在被调用时的安全性和可用性。这种结构的目的是为了形成一个单独的语句块,以便在宏被调用时能够像一个单独的语句一样使用,并且能够避免一些潜在的问题,比如多次执行或与条件语句的交互问题。

#include <stdio.h>

#define INT_VAL(x,y) do{x=0;y=0;}while(0)
int main() 
{
    int a = 10;
    int b = 12;
    
    printf("%d,%d\n", a, b);
    if (1)
        INT_VAL    (a, b);//宏定义的可以带空格,但不推荐
    else
        printf("%d,%d\n", a, b);
    printf("%d,%d\n", a, b);
    return 0;
}

经过预处理后,代码中的INT_VAL(a, b);会被替换为do { a=0; b=0; } while(0);,这会被视为一个单独的语句块。因此,ab会被分别赋值为0,而不会有其他副作用。

宏定义跟是否在函数体内没有关系,宏只是简单的替换。宏的范围从定义往后都有效。

#include <stdio.h>

void show()
{
    #define n 6
    printf("show:%d\n", n);
}

int main() 
{
    show();
    printf("main:%d\n", n);
    return 0;
}

#undef n//取消宏

#include <stdio.h>

int main()
{
#define X 3
#define Y X*2
#undef X
#define X 2
    int z = Y;//X*2 X=2 2*2=4
    printf("%d\n", z);//4 在预处理时展开为X*2,即2*2,所以 z 的值为4。
    return 0;
}
#include <stdio.h>

#define DEBUG

int main() {
#ifdef DEBUG
    printf("Debug mode is enabled\n");
#endif

#ifndef TESTING
    printf("Testing mode is disabled\n");
#endif

#ifdef TES
    printf("Tes mode is enabled\n");
#else
    printf("Tes mode is disabled\n");
#endif

    return 0;
}
#include <stdio.h>

#define VERSION 2

int main() 
{

#if VERSION==1 
    printf("VERSION1\n");
#elif VERSION==2
    printf("VERSION2\n");
#else
    printf("NO\n");

#endif

    return 0;
}
#include <stdio.h>

int main() 
{

#if define(VERSION)
    printf("yes\n");
#endif


#if !define(VERSION)
    printf("no\n");
#endif
    return 0;
}
#include <stdio.h>

#define C
#define CPP

int main() 
{

#if (defined(C) && defined(CPP))
    printf("C && CPP\n");
#else
    printf("other\n");
#endif

    return 0;
}

头文件展开

把头文件的内容去注释,条件编译后的内容拷贝到目标源文件。

#error,#line

#error 和 #line 都是C/C++预处理器指令,用于在编译前对源代码进行处理。

#error 指令

  • 功能#error 指令用于在预处理阶段生成一个错误消息,并停止编译过程。
  • 示例
    #if defined(DEBUG)
        #error Debug mode is not supported in this build.
    #endif
    

#line 指令

  • 功能#line 指令用于修改编译器输出的行号和文件名信息,对于调试和错误报告非常有用。
  • 示例
    #line 100 "custom_debug.h"
    printf("Custom debug message\n");
#include <stdio.h>

int main() 
{
    printf("%s,%d\n", __FILE__, __LINE__); // 打印当前文件名和行号

#line 100 "custom_debug.h" // 修改下一个源代码行的行号为100,文件名为"custom_debug.h"

    printf("%s,%d\n",__FILE__,__LINE__); // 打印修改后的文件名和行号

    return 0;
}

#pragma

  • 关闭特定警告

    #pragma warning(disable: 4996)
    
  • 恢复所有警告

    #pragma warning(default: all)
#pragma pack(n)

#pragma pack 指令用于设置结构体成员的对齐方式。

n:指定对齐系数,通常为1、2、4、8或者更大的值。表示结构体成员的对齐方式。

#pragma once

#pragma once 是一种头文件保护宏,用于避免同一个头文件被多次包含。这种方式与传统的头文件保护宏 #ifndef#define#endif 相比,更简洁且不易出错。

#include <stdio.h>

#define M
int main() {


#ifdef M
#pragma message("宏M定义了")
#else
#error 宏M未定义 
#endif

    return 0;
}

#pragma message 是一种预处理指令,用于在编译过程中向程序员输出一条自定义的消息。这在调试和代码开发过程中可以帮助程序员添加注释或者提醒信息。

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

#define TOSTRING(s) #s//转换为字符串

#define CONT(base,n) base##e##n //base*10^n

int main() 
{

    printf(TOSTRING(666666666666)"\n");
        
    printf("%f\n", CONT(3.14, 6));//3.14*10^6

    return 0;
}

指针指向自己

当一个指针指向自己时,可能导致以下问题和错误:

  1. 无限循环:如果代码中存在对指针自身内容的引用或修改,指针指向自身可能导致无限循环或递归调用,因为每次访问指针时都会引用自身,永远无法跳出循环。

  2. 内存泄漏:指针指向自身可能使得原本被指向的内存无法被正确释放,导致内存泄漏问题。

  3. 未定义行为:C语言标准并没有定义指针指向自身的行为,因此这种操作通常被视为未定义行为,可能导致程序崩溃、数据损坏或其他意外行为。

  4. 逻辑错误:指针指向自身可能会导致代码逻辑错误,因为通常指针用于引用或操作其他数据而不是自身。

  5. 难以维护和理解:代码中存在指针指向自身会增加代码的复杂性,使得代码难以维护和理解,降低代码的可读性和可维护性。

总的来说,指针指向自身通常不符合通常的编程实践,容易导致混乱和错误。在实际编程中,应该避免让指针指向自身,除非有非常特殊的需求或者清晰的目的。

#include <stdio.h>

int main() 
{

    int* p = NULL;
    printf("%p\n", p);
    p = (int*)&p;
    printf("%p\n", p);
    printf("%p\n", &p);

    *p = 10;
    printf("%d\n", *&p);

    p = 20;
    printf("%p\n", p);
    printf("%d\n", *&p);

    return 0;
}
#include <stdio.h>

int main() 
{
   
    int************** p = NULL;//只要是一级指针以上的指针都是4或者8字节
    printf("%d\n", p + 1);//x86:4 x64:8 

    return 0;
}
#include <stdio.h>

int main() 
{

    char* str = "hello world";//字符常量区保存

    char buf[] = "hello world";//栈上保存
    
    return 0;
}

在给定的C代码中,存在两个字符串的定义方式,分别是使用指针和使用字符数组。

  1. char* str = "hello world";

     

    这里 str 是一个指向字符的指针,指向了一个字符串常量 "hello world"。在这种情况下,"hello world" 是一个常量字符串,存储在只读内存区域,因此尝试修改 str 所指向的内容会导致未定义行为。编译器通常会发出警告,因为字符串常量应该被声明为 const char*,以避免无意修改常量内存的情况。

  2. char buf[] = "hello world";

     

    这里 buf 是一个字符数组,初始化为 "hello world"。这种方式将创建一个包含字符串内容的数组,可以对数组的内容进行修改。buf 中的内容实际上是在栈上分配的,因此可以被修改。在这种情况下,buf 是一个可变的字符数组,你可以修改数组中的内容。

总结:

  • char* str = "hello world"; 定义了一个指向常量字符串的指针,内容是只读的。
  • char buf[] = "hello world"; 定义了一个可变的字符数组,内容可以被修改。

在编程中,需要根据情况选择合适的数据类型。如果需要一个不可变的字符串,使用指向常量字符串的指针是合适的;如果需要一个可变的字符串,使用字符数组是更好的选择。

数组传参降维成内部元素类型指针

        在C语言中,数组传参确实会被转换为指向数组第一个元素的指针。这是因为在C语言中,数组名实际上是数组第一个元素的地址。当你将数组传递给函数时,传递的是数组的起始地址,即数组名被解释为一个指向数组第一个元素的指针。这种行为使得数组传参更加高效,因为不会复制整个数组。

这种降维成指针的行为是C语言的特性之一,使得数组传递更加简洁和高效。这也意味着函数在接收数组参数时,可以通过指针来访问数组的元素,从而对数组进行修改。

#include <stdio.h>

void show(char buf[])//实际char buf[]是指针char* str
{

    printf("%p\n", buf);
    printf("%p\n", &buf);

}

int main() 
{

    char buf[] = "hello world";

    printf("%p\n", buf);
    printf("%p\n", &buf);
    show(buf);

    return 0;
}
#include <stdio.h>

int main() 
{
    int a[10] = { 0 };//数组元素个数也是数组类型的一部分。 类型:int[10]

    int(*q)[10] = a;//err 类型不匹配
    int(*p)[10] = &a;//数组指针指向的是一个数组,所以要数组的地址&a。 类型:int[10]*

    return 0;
}

指针和数组指向或表示同一块空间时,访问方式是可以互通的,具有相似性没有相关性。

#include <stdio.h>

int main() 
{
    char arr[] = "hello world";
    char* p = arr;
   
    printf("arr_add:%p\n", &arr);
    printf("arr_data:%s\n", arr);
    printf("p_add:%p\n", &p);
    printf("p_date:%p\n", p);

    printf("arr_data[6]:%c\n", arr[6]);
    printf("arr_data[6]:%c\n", *(arr + 6));
    printf("p_date:%s\n", p);
    printf("p_date[6]:%c\n", p[6]);
    printf("p_date[6]:%c\n", *(p + 6));

    return 0;
}

对于二级指针及以上级别的指针执行 +1 操作时,会增加4 个字节(32 位系统)或 8 个字节(64 位系统)字节的大小。

        在 C 语言中,指针的大小取决于系统架构。通常情况下,指针的大小与系统的位数相关。在大多数现代系统中:

  • 32 位系统上指针的大小通常是 4 字节。
  • 64 位系统上指针的大小通常是 8 字节。

当你对指针执行 +1 操作时,指针会向前移动一个单位,单位的大小等于指针指向的数据类型的大小。因此,对于不同类型的指针,指针+1操作会增加的字节数是不同的。

对于一级指针而言,无论指针指向什么类型,指针+1都会增加对应类型的字节数。例如,对于 int* 指针,指针+1会增加 4 个字节。

对于二级指针(指向指针的指针)以及以上级别的指针,+1 操作会增加指针本身的大小,即指向的是另一个指针的指针。因此,对于二级指针,+1 操作会增加指针大小的字节数。

假设在一个 64 位系统上:

  • 一级指针的大小是 8 字节。
  • 二级指针的大小也是 8 字节。
  • 三级指针的大小仍然是 8 字节。

因此,对于二级指针及以上级别的指针执行 +1 操作时,会增加 8 个字节的大小。

#include <stdio.h>

int main() 
{
  
    int a = 10;
    int* p = &a;
    int** pp = &p;
    int*** ppp = &pp;

    printf("%p\n", p);
    printf("%p\n", p + 1);//相差指针指向类型的字节大小
    printf("%p\n", pp);
    printf("%p\n", pp + 1);//相差指针大小个字节
    printf("%p\n", ppp);
    printf("%p\n", ppp + 1);//相差指针大小个字节

    return 0;
}

在c中任何函数调用只要有形参实例化必定形参临时拷贝,包括数组传地址只不过发生降维拷贝的是指针

        在 C 语言中,当你调用一个函数时,如果函数有参数,这些参数会被传递给函数。对于基本数据类型(如 int、float 等),参数是通过值传递的,这意味着函数内部的操作不会影响到原始数据。但是对于数组,数组名会被转换为指向数组第一个元素的指针,因此在函数调用时,实际上是传递了数组的地址(指针),而不是整个数组的拷贝。

        在 C 语言中,对于多维数组,只能省略第一个维度,这是因为 C 语言中多维数组在内存中是按行优先顺序存储的,也就是说多维数组在内存中是连续存储的一维数组。

当你传递一个多维数组给一个函数时,如果要省略维度,只能省略最左边的维度,因为编译器需要知道如何解释数组的存储结构,以便正确计算地址偏移量。

举个例子,对于一个 int arr[2][3][4] 的三维数组,内存中的存储结构是连续的,类似于一个 int arr[24] 的一维数组。在传递这个数组给函数时,需要指定第一个维度的大小,以便编译器可以正确计算地址偏移量。

如果省略了第一个维度,编译器无法计算正确的地址偏移量,因为它无法确定每个子数组的大小以及如何跳转到每个子数组的起始位置。因此,只有最左边的维度可以被省略,其他维度必须显式指定大小。

#include <stdio.h>

void fun()
{
	printf("hello c\n");
	printf("%p\n", fun); // 输出函数 fun 的地址
}

int main()
{
	// 声明一个函数指针 p
	void(*p)();

	// 将 fun 函数的地址转换为整型数据,并将其地址赋给 p
	*(int*)&p = (int)&fun;

	// 通过函数指针调用 fun 函数
	(*(void(*)())p)();

	// 直接调用 fun 函数
	fun();

	// 输出函数 fun 的地址
	printf("%p\n", &fun);

	// 通过函数指针 p 间接调用 fun 函数
	(*p)();

	// 通过函数指针 p 间接调用 fun 函数
	p(); 

	return 0;
}

内联汇编

        内联汇编是一种在高级语言(如C、C++)代码中直接嵌入汇编指令的技术。它允许在高级语言代码中插入汇编代码块,从而可以实现对底层硬件的直接控制或者进行一些性能优化。

在C和C++中,内联汇编通常通过asm(或__asm__)关键字来实现。具体用法可能因编译器而异。

#include <stdio.h>

int main() {
    int a = 10, b = 20, result = 0;

    // 使用内联汇编将a和b相加,并将结果存储在result中
    __asm {
        mov eax, a; 将a加载到寄存器eax
        add eax, b; 将b加到eax中
        mov result, eax; 将eax中的结果存储到result中
    }

    printf("Result: %d\n", result);

    return 0;
}

函数栈帧

        函数栈帧(function stack frame)是在函数调用时在内存中动态分配的一个区域,用于存储函数执行所需的信息。函数栈帧通常由以下几个部分组成:

  1. 参数传递:函数参数通常被传递到函数栈帧中的特定位置,以便函数可以访问这些参数。参数可以传递通过寄存器或者栈。

  2. 局部变量:函数内部声明的局部变量会被存储在函数栈帧中。这些变量在函数执行期间可见,并在函数结束时被销毁。

  3. 返回地址:函数调用时,调用方程序计数器(PC)的值或者返回地址会被保存在函数栈帧中。这个地址指向函数执行完毕后要返回的位置,以便程序流可以正确返回到调用函数。

  4. 旧的帧指针:在一些体系结构中,函数栈帧可能包含指向调用函数栈帧的指针,通常称为旧的帧指针(old frame pointer)或者基指针(base pointer)。这个指针的存在有助于在函数返回时正确地恢复上一个函数的执行状态。

  5. 其他寄存器:有些体系结构需要保存其他寄存器的值,以便在函数执行期间能够正确地恢复这些寄存器的状态。这些寄存器的内容也可能会被保存在函数栈帧中。

函数栈帧的创建和销毁遵循“后进先出”(Last In First Out,LIFO)的原则。当一个函数被调用时,新的函数栈帧会被分配,并且栈指针将被调整以保持栈的正确状态。当函数执行结束时,函数栈帧将被销毁,栈指针会被移回到调用函数的栈帧。

#include <stdio.h>

int MyAdd(int a, int b)//临时变量的形成是在函数正式被调用之前就形成了 
{
    int c = 0;
    
    printf("%d\n", b);//2
    *(&a + 1) = 9;//形参实例化的顺序是从右向左的 所以b的地址比a大 相当于b = 9
    printf("%d\n", b);//9

    c = a + b;//10
    return c;
}

int main() 
{
    int x = 1;
    int y = 2;
    int z = 0;
    z = MyAdd(x, y);

    printf("z=%d\n", z);//10
    return 0;
}

可变参数列表

可变参数列表,至少要有一个明确参数。

在可变参数中,若是短整形一般要进行整型提升。

va_list

   va_list 是 char*指针类型,用于声明可变参数列表。它在 <stdarg.h> 头文件中定义,用于在可以接受可变数量参数的函数中使用。

  typedef char* va_list;

va_start()

   va_start 是 C 语言中的一个宏,用于初始化 va_list 类型的变量,使其指向参数列表中的第一个可变参数。这个宏通常与 va_end 配合使用,用于处理可变数量的参数列表。

#define va_start __crt_va_start
//将 va_start 重定向到 __crt_va_start
#define __crt_va_start(ap, x) __crt_va_start_a(ap, x)
// __crt_va_start 则被定义为调用 __crt_va_start_a
#define __crt_va_start_a(ap, v) ((void)(ap = (va_list)_ADDRESSOF(v) + _INTSIZEOF(v))) 
// 定义 __crt_va_start_a 宏

((void)(ap = (va_list)_ADDRESSOF(v) + _INTSIZEOF(v))) 

#define _ADDRESSOF(v) (&(v))
#define _INTSIZEOF(n)          ((sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1))

(void)(ap = (char*)(&(v)) + ((sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1)))

(void)(ap = (char*)(&(v)) + ((sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1))
  • (char*)(&(v)):将参数 v 的地址转换为 char* 类型的指针。
  • sizeof(n):获取参数 n 的大小(以字节为单位)。
  • sizeof(int):获取整型数据类型的大小(以字节为单位)。
  • ((sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1)):这个表达式是一个位运算操作,它将 sizeof(n) 加上一个整型数据类型大小减一的结果,然后与一个整型数据类型大小减一的取反结果进行按位与操作。这个操作的目的是将大小按照整型数据类型的对齐方式进行调整。

最终,整个表达式将计算出下一个参数在可变参数列表中的位置,并将该位置赋给 ap,同时使用 (void) 来抑制赋值表达式的返回值。用于获取可变参数列表中下一个参数的地址。

va_arg()

    va_arg 是 C 语言中的一个宏,用于从可变参数列表中逐个获取参数的值。它接受两个参数,一个是 va_list 类型的参数列表,另一个是要获取的参数的类型,然后返回该参数并将 va_list 指针指向下一个参数。

#define va_arg   __crt_va_arg
#define __crt_va_arg(ap, t)     (*(t*)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)))
  1. ap += _INTSIZEOF(t): 将指针 ap 向前移动 t 类型的大小。
  2. (ap += _INTSIZEOF(t)) - _INTSIZEOF(t): 将指针 ap 移动回到前一个参数的位置。
  3. *(t*): 将指针转换为 t* 类型,然后取该位置的值。

综合起来,这段代码的作用是从可变参数列表中获取下一个参数,并将其转换为类型 t,然后返回其值。

va_copy()

   va_copy 是 C 语言中的一个宏,用于将一个 va_list 对象的状态复制到另一个 va_list 对象中。这个宏的目的是为了在处理可变参数函数时能够保存和重新使用参数列表的状态。

#define va_copy(destination, source) ((destination) = (source))

它的作用是简单地将 source 的值赋给 destination

va_end()

    va_end 是一个 C 语言中的宏,用于结束对可变参数列表的访问。当使用可变参数函数处理完参数后,应该使用 va_end 来清理资源并结束对参数列表的访问。

#define va_end   __crt_va_end
#define __crt_va_end(ap)        ((void)(ap = (va_list)0))
  • (va_list)0: 将整数 0 转换为 va_list 类型,这通常是将一个空指针值赋给 va_list 类型的指针。
  • ap = (va_list)0: 将上述转换后的空指针值赋给 ap,从而将 ap 设置为一个空指针。
#include <stdio.h>
#include <stdarg.h>

void print_ints(int num, ...) 
{
    va_list arg;
    va_start(arg, num);

    for (int i = 0; i < num; i++)
    {
        int value = va_arg(arg, int);
        printf("%d ", value);
    }

    va_end(arg);
}

int main(int argc, char* argv[])
{
    for (int i = 0; i < argc; i++)
    {
        printf("argc[%d]=%s\n", i, argv[i]);
    }
  
    print_ints(3, 10, 20, 30);

    return 0;
} 

命令行参数

命令行参数是指在运行程序时从命令行传递给程序的参数。

#include <stdio.h>

int main(int argc, char* argv[])
{

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

    for (int i = 0; i < argc; i++)
    {
        printf("%s\n", argv[i]);
    }

    return 0;
}

遍历并打印程序的命令行参数。在C语言中,argc 表示命令行参数的数量,argv 是一个指向参数字符串数组的指针,其中 argv[0] 通常包含程序的名称,而后续的元素 argv[1]argv[2] 等包含传递给程序的参数。

在 main 函数中的 for 循环遍历了参数数组 argv,并使用 printf 函数打印每个参数的值。每次循环迭代时,argv[i] 提供了一个指向当前参数字符串的指针。

int main(int argc, char* argv[], char* env[]) 
{

    for (int i = 0; env[i]; i++)
    {
        printf("env[%d]:%s\n", i, env[i]);
    }

    return 0;
}

用于打印程序的环境变量(environment variables)。在C语言中,环境变量通常作为一个字符串数组传递给main函数的第三个参数,即char* env[]

main函数接受三个参数:argc表示命令行参数的数量,argv是一个指向参数字符串数组的指针,而env是一个指向环境变量字符串数组的指针。

递归

         递归是指一个函数在内部调用自身的过程,通常用于解决可以被分解为相同类型的更小实例的问题。在编程中,递归是一种强大的技术,特别适用于解决树形结构或者可以被分解为相同类型子问题的情况。

main函数也可以自己调用自己

#include <stdio.h>

int main()
{
    printf("Hello from main!\n");

    // 调用 main 函数自身
    main();

    return 0;
}

它会一直打印 "Hello from main!",然后无限递归调用 main 函数,直到程序耗尽栈空间触发栈溢出错误。

斐波那契数列

递归算法

#include <stdio.h>
#include <windows.h>

int fib(int n)
{
    if ((n == 1) | (n == 2)) 
    {
        return 1;
    }
    return fib(n - 1) + fib(n - 2);
}

int main()
{
    int n = 45;
    double start = GetTickCount();
    int x = fib(n);
    double end = GetTickCount();

    printf("fib(%d): %d,count: %.1f S\n", n, x, (end - start)/1000);
   
    return 0;
}

递归算法是最直接的方法,但在计算大数值时效率较低,因为存在大量重复计算。

#include <stdio.h>

int fib(int n)
{
    int first = 1;
    int second = 1;
    int third = 1;

    while (n >= 3)
    {
        third = second + first;
        first = second;
        second = third;
        n--;
    }
    return third;
}

int main()
{
    int n = 10;

    printf("%d\n", fib(n));
    return 0;
}

这种方法是一种迭代的方式来计算斐波那契数列,它避免了递归中的重复计算,效率比纯递归方法更高。

#include <stdio.h>
#include <malloc.h>

int fib(int n)
{
    int* f = (int*)malloc(sizeof(int) * (n + 1));
    if (NULL == f)
    {
        return -1;
    }
    f[1] = 1;
    f[2] = 1;
    int i = 3;

    while (i <= n)
    {
        f[i] = f[i - 1] + f[i - 2];
        i++;
    }
    int ret = f[n];
    free(f);

    return ret;
}

int main()
{
    int n = 10;

    printf("%d\n", fib(n));
    return 0;
}

这段代码实现了使用动态规划算法计算斐波那契数列。代码中使用了动态内存分配,通过 malloc 分配了一个整型数组来存储中间结果,避免了重复计算。这种方法在空间复杂度上会比较高,因为需要额外的空间来存储中间结果。

  • 17
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值