C 语言学习

167 阅读16分钟
  • 字符转 ASCII 码
char c = 'A';
printf("%c 的 ASCII 码为: %d\n", c, c);
  • 动态可变长的结构体
typedef struct
{
  int id;
  char name[0];
}stu_t;

定义该结构体,只占用4字节的内存,name不占用内存。

stu_t *s = NULL;    //定义一个结构体指针
s = malloc(sizeof(*s) + 100);//sizeof(*s)获取的是4,但加上了100,4字节给id成员使用,100字节是属于name成员的
s->id = 1010;
strcpy(s->name,"hello");

注意:一个结构体中只能有一个可变长的成员,并且该成员必须是最后一个成员。

  • 内存管理

malloc() 函数:用于动态分配内存。它接受一个参数,即需要分配的内存大小(以字节为单位),并返回一个指向分配内存的指针。

calloc() 函数:用于动态分配内存,并将其初始化为零。它接受两个参数,即需要分配的内存块数和每个内存块的大小(以字节为单位),并返回一个指向分配内存的指针。

realloc() 函数:用于重新分配内存。它接受两个参数,即一个先前分配的指针和一个新的内存大小,然后尝试重新调整先前分配的内存块的大小。如果调整成功,它将返回一个指向重新分配内存的指针,否则返回一个空指针。

  • 编译 & 执行 C 程序
1、新建 hello.c 文件,编写代码
2、键入 gcc hello.c,输入回车,编译代码。
3、如果代码中没有错误,命令提示符会跳到下一行,并生成 hello.out(Windows 生成 hello.exe)  可执行文件
4、键入 .\hello.exe 来执行程序
  • 编译多个 c 文件
$ gcc test1.c test2.c -o main.out
$ ./main.out

gcc 命令如果不指定目标文件名时默认生成的可执行文件名为 a.out(linux)  或 a.exe(windows) 。用 gcc [源文件名] -o [目标文件名] 来指定目标文件路径及文件名。

  • 因编译器的原因,生成的  .exe 文件打开时会一闪而过,从而观察不到其运行的结果,这是因为 main()  函数结束时,DOS 窗口会自动关闭。为了避免这个问题可在 return 0;  前加入 system("pause");  语句。
#include <stdio.h>
#include <stdlib.h> 
int main()
{
   /* 我的第一个 C 程序 */
   printf("Hello, World! \n");
   system("pause");      //暂停函数,请按任意键继续...
   return 0;
}
  • 自动类型转换规则

-   1、若参与运算量的类型不同,则先转换成同一类型,然后进行运算。

-   ** 

    2、转换按数据长度增加的方向进行,以保证精度不降低。如int型和long型运算时,先把int量转成long型后再进行运算。

     

    a、若两种类型的字节数不同,转换成字节数高的类型

     

    b、若两种类型的字节数相同,且一种有符号,一种无符号,则转换成无符号类型

-   ** 3、所有的浮点运算都是以双精度进行的,即使仅含float单精度量运算的表达式,也要先转换成double型,再作运算。

-   ** 4、char型和short型(在visual c++等环境下)参与运算时,必须先转换成int型。

-   ** 5、在赋值运算中,赋值号两边量的数据类型不同时,赋值号右边量的类型将转换为左边量的类型。如果右边量的数据类型长度比左边长时,将丢失一部分数据,这样会降低精度,丢失的部分直接舍去。
  • 变量不初始化

在 C 语言中,如果变量没有显式初始化,那么它的默认值将取决于该变量的类型和其所在的作用域。

对于全局变量和静态变量(在函数内部定义的静态变量和在函数外部定义的全局变量),它们的默认初始值为零。

以下是不同类型的变量在没有显式初始化时的默认值:

  • 整型变量(int、short、long等):默认值为0。
  • 浮点型变量(float、double等):默认值为0.0。
  • 字符型变量(char):默认值为'\0',即空字符。
  • 指针变量:默认值为NULL,表示指针不指向任何有效的内存地址。
  • 数组、结构体、联合等复合类型的变量:它们的元素或成员将按照相应的规则进行默认初始化,这可能包括对元素递归应用默认规则。

需要注意的是,局部变量(在函数内部定义的非静态变量)不会自动初始化为默认值,它们的初始值是未定义的(包含垃圾值)。因此,在使用局部变量之前,应该显式地为其赋予一个初始值。

总结起来,C 语言中变量的默认值取决于其类型和作用域。全局变量和静态变量的默认值为 0,字符型变量的默认值为 \0,指针变量的默认值为 NULL,而局部变量没有默认值,其初始值是未定义的。

  • C 中的变量声明 变量的声明有两种情况:

1、一种是需要建立存储空间的。例如:int a 在声明的时候就已经建立了存储空间。 2、另一种是不需要建立存储空间的,通过使用extern关键字声明变量名而不定义它。 例如:extern int a 其中变量 a 可以在别的文件中定义的。

除非有 extern 关键字,否则都是变量的定义。

extern int i; //声明,不是定义
int i; //声明,也是定义
  • 内存寻址由大到小,优先分配内存地址比较大的字节给变量,所以说变量越先定义,内存地址就越大。 如下面代码,先定义变量 a,再定义变量 b,打印出 a 的地址是 0x7fff5fbff828,b 的值是 0x7fff5fbff824。a 的地址比 b 的地址大 4 字节。
int a = 10;
int b = 20;
printf("a = %d, b = %d, pa = %p, pb = %p\n", a, b, &a,&b); // 输出变量的地址
  • 变量应先定义再赋值,在一个表达式中,左值必须是变量,右值可以是变量,常量或者表达式, 以下输出结果 是 3
int a,b;
    a=(b=3);//注意左值 等同a=b=3,但是a=(a=b)=3是错误表示
    printf("%d\n",a);//输出 3
    
    首先,表达式的最右侧 `b = 3` 执行。这意味着将整数 `3` 赋值给变量 `b`。此时,变量 `b` 的值变为 `3`。
   然后,整个赋值表达式 `b = 3` 的结果(即 `3`)被赋值给变量 `a`。这意味着变量 `a` 也将获得值 `3`。
  • 整数常量

整数常量也可以带一个后缀,后缀是 U 和 L 的组合,U 表示无符号整数(unsigned),L 表示长整数(long)。后缀可以是大写,也可以是小写,U 和 L 的顺序任意。

85         /* 十进制 */
0213       /* 八进制 */
0x4b       /* 十六进制 */
30         /* 整数 */
30u        /* 无符号整数 */
30l        /* 长整数 */
30ul       /* 无符号长整数 */
int myInt = 10;  
long myLong = 100000L;  
unsigned int myUnsignedInt = 10U;
  • const 关键字
const int var ; //error

const int var2;
var2 = 5;   //error

const int var3 = 5; //ok

x

  • #define 与 const 区别

image.png

  • 不使用三方变量交换 两个 int 的值
void swap_test() {
    int a = 10;
    int b = 20;
    printf("before swap a = %d, b = %d\n", a, b);
    //a 的新值是 10 XOR 20,这个操作将 a 存储了 1020 的位的异或结果。
    a = a ^ b;
    //新的 b 相当于 (a XOR b) XOR b。根据异或的特性,由于 b ^ b = 0,我们可以得到:b 现在是原始 a 的值,即 10
    b = a ^ b;
    //新的 a 相当于 (a XOR b) XOR a,由于 a ^ a = 0,所以得到 a = b;
    a = a ^ b;
    printf("after swap a = %d, b = %d\n", a, b);
}
  • 取余说明

取余,也就是求余数,使用的运算符是 %。C 语言中的取余运算只能针对整数,也就是说,% 的两边都必须是整数,不能出现小数,否则编译器会报错。另外,余数可以是正数也可以是负数,由 % 左边的整数决定:- ** 如果 % 左边是正数,那么余数也是正数;** 如果 % 左边是负数,那么余数也是负数;

  • 内部函数

如果一个函数只能被本文件中其他函数所调用,它称为内部函数。在定义内部函数时,在函数名和函数类型的前面加 static

  • 关于 main 函数的参数

image.png

  • 参数传递 本质上说,C 里面所有的函数参数传递,都是值传递。

指针传递之所以能改变传递参数变量的值,是因为 swap 函数交换的不是传递进来的指针本身,而是指针指向的值。

  • 可以用预处理命令 define 来定义简单函数:
#define  MAX_3(a, b, c) ( ((a > b ? a : b) > c) ? (a > b ? a : b) : c )
#define  MIN_3(a, b, c) ( ((a < b ? a : b) < c) ? (a < b ? a : b) : c )
#define  MAX_2(x, y)  ( x> y ? x : y )
#define  MIN_2(x, y)  ( x< y ? x : y )
#define  ARR_SIZE(a)  ( sizeof( (a) ) / sizeof( (a[0]) ) )
#define  MULTIPLE(m, n) ( (m%n == 0)?0:1 )
#define  AVE_3(a, b, c) (a+b+c)/3
#define  SUM_3(a, b, c) a+b+c
#define  SWAP(a, b){int t= a;a=b;b=t;}

printf("MAX_3(10,12,1):%d\n",MAX_3(10,12,1));
printf("MAX_3(10,12.1,1):%.2f\n",MAX_3(10.1,12,1));
  • 初始化局部变量和全局变量 当局部变量被定义时,系统不会对其初始化,您必须自行对其初始化。定义全局变量时,系统会自动对其初始化,如下所示:

image.png

预处理器

  • 字符串常量化运算符(#)

在宏定义中,当需要把一个宏的参数转换为字符串常量时,则使用字符串常量化运算符(#)。在宏中使用的该运算符有一个特定的参数或参数列表。

#define message_for(a,b )\
    printf(#a " and " #b ": are friends")
    
message_for(lily,lucy);
  • 标记粘贴运算符
#include <stdio.h>
 
#define tokenpaster(n) printf ("token" #n " = %d", token##n)
 
int main(void)
{
   int token34 = 40;
   
   tokenpaster(34);
   return 0;
}

void mark_paster_identifier() {
    int token34 = 40;
    tokenpaster(34);
}

//输出:
token34 is 40
  • File 操作文件模式

image.png

注意:  只有用 r+ 模式打开文件才能插入内容,w 或 w+ 模式都会清空掉原来文件的内容再来写,a 或 a+ 模式即总会在文件最尾添加内容,哪怕用 fseek() 移动了文件指针位置。

  • 格式化字符串
char content[100];
int randomNum = rand()%100 + 1;
sprintf(content, "Hi Jerry,How are you! money:%d", randomNum);
printf("%s",content);
  • 输入输出

image.png

  • 如何安全的进行 strcpy 拷贝
typedef struct Books {
    char title[5];
    char author[50];
    int price;
} Book;

strncpy(book.title, "C Programming",sizeof(book.title) -1);
// -1 确保留出一个位置给结束符
book.title[sizeof(book.title) - 1] = '\0';  // 显式添加结束符
  • .c C语言源文件 .h: 头文件(header)

  • C 语言打印 C 中打印输出使用 printf 时,需要使用 #include <stdio.h> studio.h 代表头文件, 其中 stdio: standard inout output

  • 数据类型

char 1 8位 最大值 256 short 2 16 位 最大值 65535 int 4 32 位
long 4/8 32/64 位 long long 4/8 float 4 double 8

存在这么多类型的原因是为了更加丰富地、恰当地(节省性能)地表达生活中的各种值,例如 表达年龄的数据类型 就不应该用 int,而应该用 short

  • 计算机中数据单位

bit byte kb mb gb tb pb

  • 全局变量和局部变量 局部变量的作用域是变量所在的局部范围 全局变量的作用于是整个工程

  • 常量

字面常量: 直观写出来的常量,例如直接写个 3,就属于字面常量 const 修饰的常变量 #define 定义的标识符常量, 例如 #define MAX 10, 注意:不要写成 #define MAX = 10 枚举常量:

enum SEX { MALE, FEMALE, SECRET };

  • 字符串

“hello” 这种又上引号引起来的一串字符串称为字符串字面值,或者简称字符串。 注:字符串的结束标记是一个 \0 的转义符,在计算字符串长度的时候 \0是结束标志,不算做字符串内容

  • ASCII 码表

image.png

  • sizeOf 和 strlen 返回的都是 size_t 类型,打印的时候,需要用 %zu 占位,不能用 %d  占位

    char arr1[] = "Hello";
    printf("arr1:%s, length:%zu\n", arr1, strlen(arr1)); // 输出 hello
  • 数组
int n = 10
int arr[n] = {1,2,3,4} //这种方式是错误的,数组的大小必须是常量值,这里 n 是变量值
  • 强制类型转换

  • 有符号数 规定最高位放符号位,最高位为 1: 代表正数 最高位为 0 代表负数

  • 原码、补码、反码

只要是整数(不管是整数还是负数),内存中存储的都是二进制的补码 对于正数来说, 原码、补码、反码相同 负数: 原码、补码、反码是不一样的 以 -2 为例: 原码:10000000 00000000 00000000 00000010
| 原码的符号位不变,其他位按位取反 反码:11111111 11111111 11111111 11111101
| 反码 + 1 补码:11111111 11111111 11111111 11111110

  • 常见关键字

auto: 修饰局部变量,例如 auto int a = 10,但是 auto 是可以省略的,因此基本不用 goto: register:寄存器, 默认 int a = 10, a是在内存里面的, 想要 a访问更快,加上 register,建议编译器把 a 定义成定义寄存器变量(计算机寄存器数量有限只有几十个,因此不能保证一定会放到寄存器,仅仅只是建议而已) signed: 有符号数,其实叫 signed int ,等价于 int unsigned:无符号数,永远都是正数 static: 修饰局部变量:局部变量的生命周期变长 修饰全局变量: 改变了变量的作用域,让静态的全局变量智能在自己所在的源文件内部使用,除了源文件就没法使用了 修饰函数: 改变了函数的作用域(不准确),改变了函数的链接属性, 外部链接属性 -> 内部链接属性

struct: 结构体关键字 typedof: 类型定义 -类型重定义,别名, 例如定义

  unsigned int age = 20;
  //给 unsigned int 取一个别名叫 u_int, 后面就可以直接用 u_int 替代unsigned int //的声明了
  typedef unsigned int u_int;
    u_int age2 = 20;

union: 联合体/共用体 volatile:

  • 计算器存储数据

image.png

从下往上: 访问速度越来越快,空间越来越小,造价越来越高

  • 指针

image.png

int a = 10;
&a :  取出 a 的地址
//地址也可以用变量存储,p 存放 a 的地址, p称为指针变量,类型位 int* 
int* p = &a
//通过地址找到 a:  采用 *p : p 前面的 * 代表解引用操作符, 下面的代码输出 10
printf("%p", *p)
//把 a这个地址所在的值改为 20 
*p = 20
//输出 20
printf("%d", a)

指针的名字叫 p,类型是 int*

  • 使用不同的方式输入同一个变量值不一样(待确定原因)
    short s1 = 55;
    //分别输出 555.5 ,原因还不知道
    printf("%d, %f\n", s1, s1);
  • 在方法外部获取数组长度,和在方法内部获取 arr size 长度不一致
    int arr[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
    int size = sizeof(arr)/sizeof(arr[0]);
    binearySearch(arr,size, 7);
    
    void binearySearch(int arr[],int size, int target)
{
          int size2 = sizeof(arr) / sizeof(arr[0]);

       printf("binearySearch sizeof(arr):%zu, sizeof(arr[0]:%zu, size2:%zu\n", sizeof(arr),sizeof(arr[0]),size2);


}
binearySearch 外部获取到的 arr size 为 10,在 binearySearch内部获取到的 size 为 2`arr` 是一个数组并且在函数 `binearySearch` 中使用 `sizeof(arr)` 时,你会遇到一个常见的陷阱。在函数内部,`sizeof(arr)` 并不返回数组的大小,而是返回传递给函数的参数 `arr` 的大小,这实际上是一个指针的大小,而不是原始数组的大小。

当你在函数外部声明 `int arr[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};` 时,`sizeof(arr)` 将返回整个数组的大小,即 10`int` 的大小。但是,当你将 `arr` 作为参数传递给函数时,它被退化为指向数组首元素的指针。因此,在函数内部 `sizeof(arr)` 将返回指针的大小,这通常是一个 `unsigned long` 类型,而不是数组的大小。

这就是为什么在 `binearySearch` 函数内部 `size2` 被获取为 2 的原因,这个值实际上是 `sizeof(int*) / sizeof(int)` 的结果,而不是数组的大小。在 32 位系统上,指针的大小可能是 4 字节,而在 64 位系统上,指针的大小是 8 字节。`sizeof(int)` 通常是 4 字节,所以 `sizeof(arr) / sizeof(arr[0])` 的结果可能是 2(在 64 位系统上)。

要解决这个问题,你应该避免在函数内部使用 `sizeof(arr)` 来获取数组的大小。相反,你应该将数组的大小作为参数传递给函数,就像你已经做的那样。你的 `binearySearch` 函数已经正确地将 `size` 作为参数,所以你应该使用这个参数,而不是重新计算数组的大小。

  • 字符串相关 api

获取字符串长度:

    char arr1[]  = "Welcome to C++";
    //获取字符串长度
    int len = strlen(arr1);

比较两个字符串是否相等(区分大小写):strcmp 比较两个字符串是否相等(不区分大小写):strncasecmp 比较两个字符串的前N位是否相等(不区分大小写):strncasecmp

  • 生成随机数

其值的范围从 0 到 RAND_MAX。RAND_MAX 是 <stdlib.h> 头文件中定义的一个宏,它表示 rand 函数所能生成的最大随机数。

RAND_MAX 的具体值依赖于实现,但标准规定它至少是 32767(即 2^15 - 1

rand()
  • 数据存放区域

image.png

  • size_t 类型

size_t 是一种数据类型,近似于无符号整型,但容量范围一般大于 int 和 unsigned。这里使用 size_t 是为了保证 arraysize 变量能够有足够大的容量来储存可能大的数组。size_t 类型在C语言标准库函数原型使用的很多,数值范围一般是要大于int和unsigned.

但凡不涉及负值范围的表示size取值的,都可以用size_t;比如array[size_t]。

size_t 在stddef.h头文件中定义。