Java老兵 转C语言,需要学习的点(最易懂的解释)

一、static

1.1 修饰函数内的局部变量:

void sayHi(void)
{              
  static int index = 5;
  index++;
}

多次调用sayHi函数,index = 5 只有在第一次调用的时候初始化一次,后面的多次调用,此句话就不执行了。

1.2 修饰全局变量或函数:

#include <stdio.h>
 
static int count = 10; // 全局变量
static void write_eeprom(const U8* xdr); // 全局函数

当 static 修饰全局变量或函数时,会使之的作用域限制在声明它的文件内。
只有此文件内可以调用,外部文件不可访问。

二、extern

直译 外面的

extern int num;  // 声明了一个整型变量num,它是在外面的、其他的文件中定义的。

执行此句代码时,不会为 num 分配任何存储空间,只是指示编译器 num 是在其他文件中定义的。
extern也可以用来修饰函数。

extern void write();

三、局部变量、全局变量

全局变量保存在内存的全局存储区中,占用静态的存储单元;
局部变量保存在栈中,只有在所在函数被调用时才动态地为变量分配存储单元。

默认值:
当局部变量被定义时,系统不会对其初始化,它的默认值有可能是一个垃圾值。
定义全局变量时,系统会自动对其初始化:

数据类型初始化默认值
int0
char‘\0’
float0
double0
pointerNULL

最佳实践是,无论是局部变量还是全局变量,定义的时候最好给显式地赋值一个默认值。

四、数组名

int heightArray[] = {1, 3, 4, 9}

heightArray既是数组名,也是指向数组第一个元素地址(也是数组的地址)的指针。

int* ptr = &heightArray[0];
也可以这么写
int* ptr = heightArray;

下面是我写的一个小程序,它本身没什么意义,单纯为了使大家更好的理解,数组名是个指针是什么意思:

int sumPreTwo(int* ptr);

int main() {
    int heightArray[] = {1, 3, 4, 9};

    int ret = sumPreTwo(heightArray);
    printf("sum pre two, the result is %d", ret);

    return 0;
}

int sumPreTwo(int* ptr)
{
	int first = *ptr;
	int* secondPtr = ptr + 1; // line2
	int second = *secondPtr; // line3
	return first + second;
}

输出:sum pre two, the result is 4

当然 line2和line3可以合并为一句

int second = *(ptr + 1)

由此,引发出求和的另一种写法:

#include <stdio.h>

int sum(int* ptr, int size);

int main() {
    int heightArray[] = {1, 3, 4, 9};

    int ret = sum(heightArray, 4);
    printf("sum all, the result is %d", ret);

    return 0;
}

int sum(int* array, int size)
{
    int sum = 0;
    for(int i=0; i<size; i++)
    {
        sum += *(array+i);
    }
    return sum;
}
/
输出: sum all, the result is 17

它本身也没什么意义,也是为了帮助大家更好地理解,为什么说:数组名是一个指向数组首元素的指针

五、定义一个函数,返回值是一个数组

C 语言不允许返回一个完整的数组,只能返回一个指向数组的指针。

#include <stdio.h>

int[] genArray(int size);

int main() {
    int heightArray[] = genArray(3);
    for(int j=0; j<3; j++){
        printf("%d element is: %d", j, heightArray[j]);
    }
    return 0;
}

int[] genArray(int size)
{
    int arr[size];
    for(int i=0; i<size; i++)
    {
        arr[i] = i;
    }
    return arr;
}

像上面这样的代码,不好意思,C不支持,编译就报错:
在这里插入图片描述

#include <stdio.h>

int* genArray();

int main() {
    int* heightArrayPtr = genArray();
    for(int j=0; j<3; j++)
    {
        printf("%d element is: %d \n", j, *(heightArrayPtr+j));
    }
    return 0;
}

int* genArray()
{
    static int arr[3];
    for(int i=0; i<3; i++)
    {
        arr[i] = i;
    }
    return arr;
}

六、静态数组

int staticArray[5]; // 静态数组声明
int staticArray[] = {1, 2, 3, 4, 5}; // 静态数组声明并初始化

获取数组长度:

int array[] = {1, 2, 3, 4, 5};
int length = sizeof(array) / sizeof(array[0]);

整个数组占用空间大小 / 一个元素占用空间大小 = 20字节 / 4字节 = 5个元素

七、动态数组(手动分配内存)

int size = 5;
int *dynamicArray = (int *)malloc(size * sizeof(int)); // 动态数组内存分配
// 使用动态数组
free(dynamicArray); // 动态数组内存释放

自己写了个例子:

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

int* genArray(int size);

int main() {
    int size = 5;
    int* heightArrayPtr = genArray(size);
    for(int j=0; j<size; j++)
    {
        printf("%d element is: %d \n", j, *(heightArrayPtr+j));
    }
    free(heightArrayPtr); // ※一定别忘了内存释放
    return 0;
}

int* genArray(int size)
{
    int* array = (int*)malloc(size * sizeof(int));

    for(int i=0; i<size; i++)
    {
        array[i] = i;
    }
    return array;
}
//
0 element is: 0
1 element is: 1
2 element is: 2
3 element is: 3
4 element is: 4

malloc 的参数是字节数,此函数在 stdlib.h 中声明。
分配内存的函数,还有calloc,它们的区别是:

  • malloc 只分配内存,并不初始化,默认值可能是一个垃圾值;
  • calloc 分配内存,并初始化值为0;
  • calloc 的参数是两个,元素数与每个元素占用的空间;
  • 而malloc只有一个占用空间字节数参数;

它们在 stdlib.h中的声明如下:

  void *__cdecl calloc(size_t _NumOfElements,size_t _SizeOfElements);
  void __cdecl free(void *_Memory);
  void *__cdecl malloc(size_t _Size);

八、枚举

enum DAY
{
      MON=1, TUE, WED, THU, FRI, SAT, SUN
};
// 值 1 2 3 4 5 6 7
enum season {spring, summer=3, autumn, winter};
// 值 0 3 4 5

enum(枚举)类型的值只能是整数类型,包括int、short、char等。你不能直接在enum中指定浮点数(如float或double)作为其值。
如果想创建浮点型枚举,可以参考以下实现:

float floatValues[3] = {0.1, 0.2, 0.3};  
enum {  
    FLOAT_VALUE_1,  
    FLOAT_VALUE_2,  
    FLOAT_VALUE_3  
};

8.1 整数转换为枚举

#include <stdio.h>

enum day
{
    saturday,
    sunday,
    monday
} workday;

int main()
{
    int day1value = 1;
    enum day weekend;
    weekend = (enum day) day1value;  //类型转换
    // weekend = day1value; // 两种类型,不能直接赋值
    printf("weekend: %d", weekend);
    return 0;
}

九、空指针

int  *ptr = NULL; 

NULL 指向0x0系统保留的地址,应用程序不能访问。在程序中表达的意思是,此指针没有指向有效值(“没有初始化”)。

十、函数指针类型与变量

声明一个函数指针类型:

typedef int (*AddNum)(int);

typedef 表示我要定义一个类型。AddNum表示这个类型的名字。* 表示这是个函数指针类型。

int xxx(int)

这个范式,表达了定义的这个函数是什么样的函数,第一个int是返回值,第二个int是参数列表。xxx函数名是任意名,不是重点。
简单理解可以把它当成一个函数的模板,就是说我定义的这个函数指针类型,它指向的函数是一个怎样的函数:它的返回值是什么类型,它的参数列表是怎样的。
它定义了某种函数的模板,它代表了一堆这样的函数。

回顾一般指针的使用:

int a = 3;
int* ptr = &a;

要想定义一个指向整型的指针变量,首先要有整型这个类型(即 int),由于int 是C语言帮我们封装好了,所以可以直接用。
但是当要定义一个指向函数的指针变量时,并没有这个函数类型,所以才需要我们自己先声明一种函数类型。int xxx(int)这个范式就描述了这种函数类型。

写了个例子,便于加深理解:

#include <stdio.h>

int addNum(int);
int ori = 0;
typedef int (*AddNum)(int); // 定义函数指针类型
void sayhello(AddNum);

int main() {
    AddNum an = &addNum; // 创建一个函数指针变量,它指向了addNum函数
    printf("hello!! %d \n", an(10)); // 调用函数指针

    for(int i=0; i<3; i++)
    {
        sayhello(an);
    }
    return 0;
}

int addNum(int add)
{
    ori += add;
    return ori;
}

void sayhello(AddNum addNum)
{
    printf("hello!! %d \n", addNum(1));
}

十一、共用体

union Data
{
   int i;
   float f;
   char  str[20];
};

Data中所有变量,共用一块内存地址。
i 占4字节,f 占4字节,str占20字节,所以Data变量占用空间为20字节(等于所有成员变量中占用空间最大的)。

int main( )
{
   union Data data;        
 
   data.i = 10;
   data.f = 220.5;
   strcpy( data.str, "C Programming");
 
   printf( "data.i : %d\n", data.i);
   printf( "data.f : %f\n", data.f);
   printf( "data.str : %s\n", data.str);
 
   return 0;
}
当上面的代码被编译和执行时,它会产生下列结果:

data.i : 1917853763
data.f : 4122360580327794860452759994368.000000
data.str : C Programming

可以看到,共用体的 i 和 f 成员的值有损坏。
因为共用内存地址,当写 str 值的时候,20个字节全重写了一遍,所以i、f 的值被覆盖掉了。

十二、typedef vs #define

它们有以下几点不同:

typedef 仅限于为类型定义符号名称,#define 不仅可以为类型定义别名,也能为数值定义别名,比如您可以定义 1 为 ONE。
typedef 是由编译器执行解释的,#define 语句是由预编译器进行处理的。

#define

在C语言中,#define是一个预处理指令,用于在编译之前进行文本替换。预处理器会在编译之前处理源代码,根据#define指令来替换代码中的文本。

例如,以下代码:

#define PI 3.14159

在编译之前,在代码中的任何地方出现的PI,它都会被预处理器替换为3.14159

除了简单的文本替换,#define还可以用于定义常量、宏函数等。例如:

#define SQUARE(x) ((x) * (x))

上述宏定义了一个名为SQUare的宏函数,该函数接受一个参数x并返回其平方。当你在代码中使用SQUare(5)时,它会被预处理器替换为((5) * (5))

typedef

在C语言中,typedef是一个关键字,它本身并不创建新的数据类型,只是为已有的数据类型创建了一个同义词或别名。

编译器在编译时解析typedef声明,并在符号表中记录这个新的类型名称与其对应的基础类型。这样,在后续的代码中,每当编译器遇到这个新的类型名称时,它都会用基础类型来替换它。

例如,考虑以下typedef声明:

typedef int Integer;
typedef struct {
    int x;
    int y;
} Point;

对于第一个typedef,编译器会记住Integerint的一个别名。在后续的代码中,任何使用Integer的地方都会被当作int来处理。

对于第二个typedef,编译器会创建一个结构体类型,并为它分配一个别名Point。这样,在代码中就可以使用Point来声明该结构体的变量,而不需要每次都写出完整的结构体定义。

需要注意的是,typedef的作用域是局部的,它遵循C语言的作用域规则。如果在一个函数内部使用typedef定义了一个类型别名,那么这个别名只在该函数内部有效。在函数外部,编译器将不认识这个别名,除非在外部也有相应的typedef声明。

十三、输入&输出

C 语言把所有的设备都当作文件
scanf() 函数用于从标准输入(键盘)读取并格式化, printf() 函数发送格式化输出到标准输出(屏幕)

#include <stdio.h>      // 标准io库
int main()
{
    printf("hello");
    return 0;
}

stdio.h 是一个头文件 (标准输入输出头文件) and #include 是一个预处理命令,用来引入头文件。 当编译器遇到 printf() 函数时,如果没有找到 stdio.h 头文件,会发生编译错误。

printf() %d 匹配整型,%f 匹配float,%p 匹配指针,%s匹配字符串,%c匹配字符

getchar() & putchar() 函数

getchar()读取下一个可用的字符,并把它返回为一个整数。这个函数在同一个时间内只会读取一个单一的字符。
int putchar(int c) 函数把字符输出到屏幕上,并返回相同的字符。这个函数在同一个时间内只会输出一个单一的字符

gets() & puts() 函数

char *gets(char *s) 函数从 stdin 读取一行到 s 所指向的缓冲区,直到一个终止符或 EOF。
int puts(const char *s) 函数把字符串 s 和一个尾随的换行符写入到 stdout。

scanf() 和 printf() 函数

带格式的输入输出

#include <stdio.h>
int main( ) {
 
   char str[100];
   int i;
 
   printf( "Enter a value :");
   scanf("%s %d", str, &i);
 
   printf( "\nYou entered: %s %d ", str, i);
   printf("\n");
   return 0;
}

十四、文件读写

#include<stdio.h>

int main()
{
    FILE *f = NULL;
    f = fopen("D:/2-Language/C/C-project1/tmp/test.txt", "w+");
    fprintf(f, "Hello World! \n");
    fputs("this is test for fputs...\n", f);
    fclose(f);
}
int fputc( int c, FILE *fp );  // 写一个字符
int fputs( const char *s, FILE *fp ); // 写一个字符串
int fprintf(FILE *fp,const char *format, ...)  // 把一个格式化字符串写入到文件中

int fgetc( FILE * fp ); // 读单个字符
char *fgets( char *buf, int n, FILE *fp ); // 读n-1个字符到buf,直到换行或文件结尾
int fscanf(FILE *fp, const char *format, ...) // 读到第一个空格或换行为止

十五、预处理器

C 预处理器不是编译器的组成部分,但是它是编译过程中一个单独的步骤。简言之,C 预处理器只不过是一个文本替换工具而已,它们会指示编译器在实际编译之前完成所需的预处理。我们将把 C 预处理器(C Preprocessor)简写为 CPP。

所有的预处理器命令都是以井号(#)开头。

#define 定义宏
#include 包含一个源代码文件

15.1 预定义宏

ANSI C 定义了许多宏。在编程中您可以使用这些宏,但是不能直接修改这些预定义的宏。

描述
__DATE__当前日期,一个以 “MMM DD YYYY” 格式表示的字符常量。
__TIME__当前时间,一个以 “HH:MM:SS” 格式表示的字符常量。
__FILE__这会包含当前文件名,一个字符串常量。
__LINE__这会包含当前行号,一个十进制常量。

15.2 预处理器运算符

宏延续运算符(\)
字符串常量化运算符(#)

十六、头文件

A simple practice in C 或 C++ 程序中,建议把所有的
常量、宏、系统全局变量和函数原型
写在头文件中,在需要的时候随时引用这些头文件。

引用头文件相当于复制头文件的内容
引用头文件会在预编译阶段,进行处理。

16.1 只引用一次头文件

如果一个头文件被引用两次,编译器会处理两次头文件的内容,这将产生错误。为了防止这种情况,标准的做法是把文件的整个内容放在条件编译语句中,如下:

#ifndef HEADER_FILE
#define HEADER_FILE

the entire header file file

#endif

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

十七、错误处理

C 语言不提供对错误处理的直接支持,但是作为一种系统编程语言,它以返回值的形式允许您访问底层数据。在发生错误时,大多数的 C 或 UNIX 函数调用返回 1 或 NULL,同时会设置一个错误代码 errno,该错误代码是全局变量,表示在函数调用期间发生了错误。您可以在 errno.h 头文件中找到各种各样的错误代码。

所以,C 程序员可以通过检查返回值,然后根据返回值决定采取哪种适当的动作。

  • perror() 函数显示您传给它的字符串,后跟一个冒号、一个空格和当前 errno 值的文本表示形式。
  • strerror() 函数,返回一个指针,指针指向当前 errno 值的文本表示形式。
#include <stdio.h>
#include <errno.h>
#include <string.h>
 
extern int errno ;
 
int main ()
{
   FILE * pf;
   int errnum;
   pf = fopen ("unexist.txt", "rb");
   if (pf == NULL)
   {
      errnum = errno;
      fprintf(stderr, "错误号: %d\n", errno);
      perror("通过 perror 输出错误");
      fprintf(stderr, "打开文件错误: %s\n", strerror( errnum ));
   }
   else
   {
      fclose (pf);
   }
   return 0;
}
///
错误号: 2
通过 perror 输出错误: No such file or directory
打开文件错误: No such file or directory

17.1 程序退出状态

exit(EXIT_SUCCESS);
exit(EXIT_FAILURE);

<stdlib.h>
#define EXIT_SUCCESS 0
#define EXIT_FAILURE 1

十八、可变参数(不常用,了解下就行)

int func_name(int arg1, ...);
#include <stdio.h>  
#include <stdarg.h>  
  
// 定义一个可变参数函数  
void print_numbers(int count, ...) {  
    va_list args; // 定义一个va_list类型的变量  
    va_start(args, count); // 初始化args,使其指向第一个可变参数  
  
    for (int i = 0; i < count; i++) {  
        int value = va_arg(args, int); // 获取下一个整数参数  
        printf("%d ", value);  
    }  
  
    va_end(args); // 清理va_list变量  
}  
  
int main() {  
    print_numbers(3, 1, 2, 3); // 输出: 1 2 3  
    print_numbers(5, 4, 5, 6, 7, 8); // 输出: 4 5 6 7 8  
    return 0;  
}

使用va_list、va_start、va_arg和va_end宏来处理可变参数。这三个宏,在<stdarg.h>中定义。

十九、内存分配

序号函数描述
1void *calloc(int num, int size);在内存中动态地分配 num 个长度为 size 的连续空间,并将每一个字节都初始化为 0。所以它的结果是分配了 num*size 个字节长度的内存空间,并且每个字节的值都是 0。
2void free(void *address);该函数释放 address 所指向的内存块,释放的是动态分配的内存空间。
3void *malloc(int num);在堆区分配一块指定大小的内存空间,用来存放数据。这块内存空间在函数执行完成后不会被初始化,它们的值是未知的。
4void *realloc(void *address, int newsize);该函数重新分配内存,把内存扩展到 newsize。

void * 类型表示未确定类型的指针。C、C++ 规定 void * 类型可以通过类型转换强制转换为任何其它类型的指针。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
 
int main()
{
   char name[100];
   char *description;
 
   strcpy(name, "Zara Ali");
 
   /* 动态分配内存 */
   description = (char *)malloc( 30 * sizeof(char) );
   if( description == NULL )
   {
      fprintf(stderr, "Error - unable to allocate required memory\n");
   }
   else
   {
      strcpy( description, "Zara ali a DPS student.");
   }
   /* 假设您想要存储更大的描述信息 */
   description = (char *) realloc( description, 100 * sizeof(char) );
   if( description == NULL )
   {
      fprintf(stderr, "Error - unable to allocate required memory\n");
   }
   else
   {
      strcat( description, "She is in class 10th");
   }
   
   printf("Name = %s\n", name );
   printf("Description: %s\n", description );
 
   /* 使用 free() 函数释放内存 */
   free(description);
}
 输出:
Name = Zara Ali
Description: Zara ali a DPS student in class 10th
strcpy( description, "Zara ali a DPS student."); // copy
strcat( description, "She is in class 10th"); // 拼接

C 语言中常用的内存管理函数和运算符
malloc() 函数:用于动态分配内存。它接受一个参数,即需要分配的内存大小(以字节为单位),并返回一个指向分配内存的指针。

free() 函数:用于释放先前分配的内存。它接受一个指向要释放内存的指针作为参数,并将该内存标记为未使用状态。

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

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

sizeof 运算符:用于获取数据类型或变量的大小(以字节为单位)。

指针运算符:用于获取指针所指向的内存地址或变量的值。

& 运算符:用于获取变量的内存地址。

  • 运算符:用于获取指针所指向的变量的值。

-> 运算符:用于指针访问结构体成员,语法为 pointer->member,等价于 (*pointer).member。

memcpy() 函数:用于从源内存区域复制数据到目标内存区域。它接受三个参数,即目标内存区域的指针、源内存区域的指针和要复制的数据大小(以字节为单位)。

memmove() 函数:类似于 memcpy() 函数,但它可以处理重叠的内存区域。它接受三个参数,即目标内存区域的指针、源内存区域的指针和要复制的数据大小(以字节为单位)。

本文完。


觉得有用的话,动动你发财的小手手,点个赞,^(∩_∩)^ 谢谢~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

洛克Lee

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值
>