字符数组和指针
- 当你写char a[] = “abcd”;时,这实际上会将字符串常量"abcd"中的内容复制到字符数组a中。这并不是将字符串常量的地址赋给a的指针,而是将字符串"abcd"的内容存储到a所代表的字符数组中。这意味着a是一个包含字符数组"abcd"内容的字符数组。此处的字符数组中的值存储在堆内存中
- 当写char* a=“abcd”; 时,实际上会将字符串常量“abcd”的地址赋给指针a,此处的字符串常量位于常量区中
二维数组的存储
- 二维数组通常是逻辑上的二维,其在物理存储单元上依旧是一维的线性存储,只是通过多个指针指向了逻辑上每行的第一个存储单元
- 二维数组中的“二维”通常指的是我们在逻辑编程中,访问数组元素是通过行和列的方式来进行访问的
- 二维数组通过指针的方式,将一长串连续的存储单元分为逻辑上的行,并将每行看成是新的数组,有助于我们编程时能够更快的进行数值访问。
- 二维数组可以通过自己开辟连续的存储空间,并将该存储空间划分为等长份的空间段,再用指针指向各段空间。
关于sizeof运算符
- sizeof()运算符中的表达式不会发生计算,它只是用来求目标所占存储空间的大小
int main()
{
short s = 0;
int a = 10;
printf("%d\n",sizeof(s=a+5));
printf("%d\n,s");
}//输出结果为2,0
按位取反
- 数据在内存中存储的是原数据的补码
- 原码通过按位取反得到反码,反码加1得到补码
- 正数的原,反,补码均为本身
- 对于~0==-1
由于0的补码是自身,那么在内存中存储的是32个0
其按位取反位32个1
由于内存中存储的是补码,那么其反码为31个1外加第一位为0
则原码为30个0加上首位末位为1
隐形数据类型转换
整数提升(Integer Promotion):
当 char、short 或 unsigned char 等较小整数类型参与运算时,它们会被自动提升为 int 类型。
示例:
c
char c = 10;
int i = 20;
int result = c + i; // char c 被提升为 int 类型,然后与 int i 相加
算术转换(Usual Arithmetic Conversion):
当两个操作数类型不同时,会根据一组规则将它们转换为相同类型。
示例:
c
int i = 10;
float f = 20.5;
float result = i + f; // int i 被转为 float 类型,然后与 float f 相加
赋值转换(Assignment Conversion):
在赋值操作中,右侧的值会被转换为左侧的类型。
示例:
c
int i = 1000;
char c = i; // int i 被截断为 char 类型,可能导致数据丢失
函数调用中的隐式转换:
当调用函数时,如果函数的参数类型与传递的参数类型不匹配,会发生隐式转换。
示例:
c
double calculateAverage(int a, int b) {
return (a + b) / 2.0;
}
int x = 5;
int y = 10;
double avg = calculateAverage(x, y); // 传递的 int 参数会被转换为 double 类型
指针
指针类型
- 指针类型决定了指针进行解引用操作的时候,能够访问空间的大小
int* p; *p可以访问4个字节
char* p; *p可以访问1个字节
double* p; \*p可以访问8个字节
- 此可访问空间大小取决于int,char,double本身所占空间的大小
指针加减整数
- 指针加减整数n会将地址偏移指针类型可访问字节数*n个字节
int *p
p = 0x11111111
p+2==0x11111119//2*4=8;int\* 可以访问4个字节;对应的int所占4字节
指针与数组
- 数组名是数组首元素地址
- &arr- &数组名-数组名部署首元素的地址-数组名表示整个数组-&数组名 取出的是整个数组的地址
- sizeof(arr) - sizeof(数组名) - 数组名表示的整个数组-sizeof(数组名)计算的是整个数组的大小
int arr[5]//arr是一个5个元素的整形数组
int *parr1{10}//parr1是一个数组,数组有10个元素,每个元素的类型是int*,parr1是指针数组
int (*parr2)[2]//parr2是一个指针,该指针指向了一个数组,数组有10个元素,每个元素的类型是int-parr2是数组指针
int (*parr3[10])[5]//parr3是一个数组,该数组有10个元素,每个元素是一个数组指针,该数组指针指向的数组有5个元素,每个元素是int
函数指针
- 函数指针是指向函数的指针,它可以存储函数的地址,允许在程序运行时动态地调用函数。在C和C++等编程语言中,函数指针可以用来实现回调函数、动态函数调用等功能。通过函数指针,可以将函数作为参数传递给其他函数,也可以在运行时根据需要改变调用的函数。
#include <stdio.h>
// 定义一个函数
void sayHello() {
printf("Hello, World!\n");
}
int main() {
// 声明一个函数指针,指向无返回值、无参数的函数
void (*funcPtr)();
// 将函数地址赋给函数指针
funcPtr = sayHello;
// 通过函数指针调用函数
funcPtr();
return 0;
}
在这个示例中,funcPtr 是一个指向无返回值、无参数的函数的指针。通过将 sayHello 函数的地址赋给 funcPtr,我们可以通过 funcPtr() 调用 sayHello 函数
- 函数指针数组的应用(计算器)
#include<stdio.h>
void menu()
{
printf("************************\n");
printf("**1.add\t2.sub**\n");
printf("**3.mul\t4.div**\n");
printt("**\t0.exit\t**");
}
int Add(int x,int y)
{
return x+y;
}
int Sub(int x,int y)
{
return x-y;
}
int Mul(int x,int y)
{
return x*y;
}
int Div(int x,int y)
{
return x/y;
}
int main()
{
int input = 0;
int x=0;
int y=0;
int (*pfArr[5])(int,int)={0,Add,Sub,Mul,Div};//转移表
do
{
menu();
printf("请选择:");
scanf("%d",&input);
if(input>=1&&input<=4)
{
printf("请输入两个操作数:");
scanf("%d%d",&x,&y);
int ret = prArr[input](x,y);
printf("%d\n",ret);
}
else if(input==0)
printf("退出");
else
printf("err");
}
}
在程序的主循环中,根据用户输入的选择,通过调用相应的函数来执行加法、减法、乘法或除法操作。这样通过函数指针数组,可以实现根据用户选择动态调用不同的函数,从而实现了代码的灵活性和可扩展性。
回调函数
- 回调函数是指将一个函数作为参数传递给另一个函数,并在该函数内部被调用。这种机制允许您在运行时动态地指定要执行的特定代码。回调函数通常用于事件处理、异步任务和通用接口设计
#include <stdio.h>
// 回调函数的原型
typedef void (*CallbackFunction)(int);
// 接受回调函数作为参数的函数
void performOperation(int data, CallbackFunction callback)
{
printf("Performing operation with data: %d\n", data);
// 调用回调函数
callback(data);
}
// 回调函数示例
void callbackFunction(int result)
{
printf("Callback function called with result: %d\n", result);
}
int main()
{
int data = 10;
// 调用 performOperation,并传递回调函数
performOperation(data, callbackFunction);
return 0;
}
结构体
- 函数传递的时候,参数是需要压栈的。如果传递一个结构体对象的时候,结构体过大,参数压栈的系统开销会比较大。
- 结构体传参时,要传结构体的地址。
- 栈区:局部变量、函数的形式参数、函数调用也开辟空间
默认由高到低使用地址 - 堆区:动态内存分配、malloc/free,realloc,calloc
- 静态区:全局变量、静态变量
数据的存储
类型的意义:
- 使用这个类型开辟内存空间的大小(大小决定了适用范围)
- 决定了看待内存空间的视角(如:使用int和float存储相同数据内存数据会不一样)
数值计算
- 内存中用补码对整数数据进行存储
- 使用补码可以让减法更加方便(cpu只有加法器)
- 1-1
1+(-1)
//原码相加
00000000000000000000000000000001
10000000000000000000000000000001
10000000000000000000000000000010
//补码相加
00000000000000000000000000000001
11111111111111111111111111111111
00000000000000000000000000000000
大小端存储
- 大端模式,数据的地位保存在内存的高地址中,而数据的高位保存在内存的低地址中
- 小端模式,是指数据的低位保存在内存的低地址中,而数据的高位,保存在内存的高地址中
整形提升
#include<stdio.h>
int main()
{
char a=-1;
singned char b=-1;
unsigned char c=-1;
printf("a=%d,b=%d,c=%d",a,b,c);
return 0;
}//输出为-1,-1,255
- 对于char a=-1
在内存中a的数据为11111111;补码表示
由于打印数据为int则发生整形提升
char为有符号整型,则在a前面填充24个符号位数值1
得到(int)a内存数据为:11111111111111111111111111111111(此处依旧为补码)
将该补码同样的转换回原码时得到10000000000000000000000000000001即-1 - 对于singned char b=-1同上
- 对于unsigned char c=-1
与char a=-1类似
只是由于unsigned为无符号数,则在整形提升时填充24个0(无符号默认正数)
得到了(int)c内存数据为:0000000000000000000000011111111(补码)
计算原码得到255
浮点数
- 浮点数在计算机内存中的存储通常遵循IEEE 754标准。根据这个标准,一个浮点数通常由三部分组成:符号位、指数部分和尾数部分。
- 符号位:用于表示数的正负,占据一个比特位,0代表正数,1代表负数。
- 指数部分:用于表示数的阶码(指数),通常以偏移值的形式存储,这个偏移值可以确保指数可以表示负数。指数部分用来调整浮点数的数量级。
- 尾数部分:也称为尾数或尾数部分,用来存储浮点数的有效数字部分,决定了浮点数的精度。
- 单精度浮点数(32位)通常按照以下格式存储:
符号位(1位)
指数部分(8位)
尾数部分(23位) - 双精度浮点数(64位)通常按照以下格式存储:
符号位(1位)
指数部分(11位)
尾数部分(52位)
int main()
{
float f = 5.5;
//5.5
//101.1;二进制写法
//(-1)^0*1.011*2^2;IEEE 754写法
//S=0;符号位
//E=2;指数部分,此处加上偏移量127(2^7-1)为129即100000001
//M=1.011;尾数部分
//0 10000001 01100000000000000000000;内存存储数据
}
字符串与内存函数
字符串函数
strlen
- size_t strlen(const char * str);
- 字符串以’\0’作为结束标志,strlen函数返回的是在字符串中’\0’前面出现的字符个数 (不包含’\0’)。
- 参数指向的字符串必须要以\0’结束,
- 注意函数的返回值为siz_t,是无符号的
strcpy
- char* strcpy(char * destination,const char * source);
- 源字符串必须以’\0’结束。
- 会将源字符串中的’\0’拷贝到目标空间
- 目标空间必须足够大,以确保能存放源字符串
- 目标空间必须修改
strcat
- char * strcat(char * destination,const char* source)
- 源字符串必须以’\0’结束。
- 目标空间必须足够大,以确保能存放新字符串
- 目标空间必须修改
strcmp
- int strcmp(const char* str1,const char * str2)
- 第一个字符串大雨第二个字符串,返回大于0的数字
- 第一个字符串等于第二个字符串返回0
- 第一个字符串小于第二个字符串,返回小于0的数字
strstr
- char* strstr(const char * ,const char *);查找字符串
strtok
- char * strtok(char * str,const char * sep)
- sep参数是给字符串,定义了用作分隔符的字符集合
- 第一个参数指定一个字符串,它包含了0个或者多个由sep字符串中一个或者多个分隔符的标记
- strtok函数找到str中的下一个标记,并将其用\0结尾,返回一个指向这个标记的指针
- strtok函数的第一个参数部位NULL,函数将找到str中第一个标记,strtok函数将保存它在字符串中的位置。
- strtok函数的第一个参数为NULL,函数将在同一个字符串中被保存的位置开始,查找下一个标记。
- 如果字符串中不存在更多的标记,则返回NULL指针
内存函数
memcpy
- void * memcpy(void * destination,const void * source,size_t num)
- 函数memcpy从source的位置开始向后复制num个字节的数据到destionation的内存位置。
- 这个函数在遇到’\0’的时候并不会停下来
- 如果source和destination有任何的重叠,复制的结果都是未定义的
memmove
- void *memmove(void *dest, const void *src, size_t n);
- (能够处理内存重叠区域的复制类似memcpy)
memcpy
- int memcmp(const void *ptr1, const void *ptr2, size_t num);
- 按字节比较 ptr1 和 ptr2 所指向的内存区域的前 num 个字节。
- 如果两个内存区域在前 num 个字节上完全相同,则返回值为0;
- 如果不同,返回值为第一个不同字节的差值(ptr1 的字节值减去 ptr2 的字节值)。
memset
- void *memset(void *ptr, int value, size_t num);
- 用于设置一块内存区域的值
- 注意此处num表示为字节数
注意上述函数中的修改区域是按某数据类型还是字节划分
#include <stdio.h>
#include <string.h>
int main() {
int arr[5]={0};
memset(arr, 1, sizeof(arr));
return 0;
}
//会使得arr内存空间为(0x)01 01 01 01 01 01 01 01 01 01...
动态内存分配
malloc和free
- void* malloc(size_t size);
- 这个函数会在堆内存中分配一块大小为 size 字节的内存,并返回一个指向这块内存的指针。
- 如果分配成功,malloc会返回一个指向分配的内存的指针;如果分配失败,它会返回NULL。
- 当不再使用动态申请的空间用free来释放
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
int main() {
int *ptr;
int n = 5;
// 分配内存
ptr = (int*)malloc(n * sizeof(int));
if (ptr == NULL) {
printf("%S\n",strerror(errno));//打印错误原因
return 1;
}
// 向分配的内存中写入数据
for (int i = 0; i < n; i++) {
ptr[i] = i + 1;
}
// 打印数据
for (int i = 0; i < n; i++) {
printf("%d ", ptr[i]);
}
// 释放内存
free(ptr);
return 0;
}
calloc
- void *calloc(size_t num, size_t size);
- 用于动态分配内存空间,并将分配的内存空间初始化为零。
- 使用同malloc。
realloc
- void *realloc(void *ptr, size_t size);
- 用于更改先前由malloc、calloc或realloc分配的内存块的大小
- ptr是指向先前分配的内存块的指针。
- size是要重新分配的内存块的新大小(以字节为单位)。
- 如果ptr是NULL,则realloc的行为类似于malloc,分配一个新的内存块,并返回指向这个新内存块的指针。
- 如果size为0,realloc的行为类似于free,释放ptr指向的内存块,并返回NULL。
- 如果ptr不是NULL且size不为0,realloc会尝试重新分配ptr指向的内存块为新的大小。如果重新分配成功,返回指向重新分配后的内存块的指针;如果失败,返回NULL,并且原来的内存块保持不变。
- realloc可能会移动内存块的位置。因此,应该始终将realloc的返回值分配给一个新指针,并且在重新分配后不再使用原来的指针。
- 在重新分配失败时,原来的内存块仍然有效,因此应该检查realloc的返回值来确保内存重分配成功。
#include <stdio.h>
#include <stdlib.h>
int main() {
int *ptr;
// 分配初始内存块
ptr = (int *)malloc(5 * sizeof(int));
if (ptr == NULL) {
printf("Memory allocation failed.\n");
return 1;
}
// 重新分配内存块的大小为10个整数
ptr = (int *)realloc(ptr, 10 * sizeof(int));
if (ptr == NULL) {
printf("Memory reallocation failed.\n");
return 1;
}
// 使用重新分配后的内存块
for (int i = 0; i < 10; i++) {
ptr[i] = i;
}
// 释放内存块
free(ptr);
return 0;
}
柔性数组(c99)
- 柔性数组只能作为结构体的最后一个成员。
- 柔性数组的大小必须在运行时确定,因为它的大小是结构体大小加上柔性数组的元素个数的总和。
- 内存分配时需要考虑柔性数组的大小。
#include <stdio.h>
#include <stdlib.h>
struct flex_array {
int length;
int data[]; // 柔性数组
};
int main() {
int i;
int array_size = 5;//数组大小
// 分配内存给结构体和柔性数组
struct flex_array *my_array = malloc(sizeof(struct flex_array) + array_size * sizeof(int));
my_array->length = array_size;
// 初始化数组
for (i = 0; i < array_size; i++) {
my_array->data[i] = i * 10;
}
// 访问和打印数组元素
for (i = 0; i < my_array->length; i++) {
printf("Element %d: %d\n", i, my_array->data[i]);
}
// 释放内存
free(my_array);
return 0;
}
文件操作
打开文件:
- 使用fopen()函数来打开一个文件,语法如下:
- FILE *fopen(const char *filename, const char *mode);
- 其中,filename是要打开的文件名,mode指定打开文件的模式,比如"r"表示只读,"w"表示写入(如果文件不存在则创建新文件),"a"表示追加等。
#include <stdio.h>
int main() {
FILE *file = fopen("example.txt", "r");
if (file == NULL) {
printf("Error opening file.\n");
return 1;
}
// 文件操作...
fclose(file);
return 0;
}
关闭文件:
- 使用fclose()函数关闭打开的文件,语法如下:
- int fclose(FILE *stream);
读写文件:
- 可以使用fscanf()和fprintf()函数进行文件的读写操作,也可以使用fread()和fwrite()函数进行二进制数据的读写。
//读文件
#include <stdio.h>
int main() {
FILE *file = fopen("example.txt", "r");
if (file == NULL) {
printf("Error opening file.\n");
return 1;
}
int num;
fscanf(file, "%d", &num);
printf("Read number: %d\n", num);
fclose(file);
return 0;
}
//写文件
#include <stdio.h>
int main() {
FILE *file = fopen("example.txt", "w");
if (file == NULL) {
printf("Error opening file.\n");
return 1;
}
fprintf(file, "Hello, World!\n");
fclose(file);
return 0;
}
定位文件指针:
- 使用fseek()函数可以移动文件指针到指定位置,语法如下:
- int fseek(FILE *stream, long offset, int whence);
#include <stdio.h>
int main() {
FILE *file = fopen("example.txt", "r");
if (file == NULL) {
printf("Error opening file.\n");
return 1;
}
fseek(file, 0, SEEK_END); // 将文件指针移动到文件末尾
// 获取文件大小
long size = ftell(file);
printf("File size: %ld bytes\n", size);
fclose(file);
return 0;
}
检查文件结尾:
- 使用feof()函数可以检查文件是否已经到达结尾。
删除文件:
- 使用remove()函数可以删除指定的文件,语法如下:
int remove(const char *filename);
#include <stdio.h>
int main() {
if (remove("example.txt") == 0) {
printf("File deleted successfully.\n");
} else {
printf("Error deleting file.\n");
}
return 0;
}
c语言预处理
程序的执行过程
-
编辑(Edit): 程序员编写源代码文件,通常使用文本编辑器来编写代码。这个阶段主要是编写代码并保存在文件中。
-
预处理(Preprocess): 在编译之前,源代码会经过预处理器处理。预处理器会执行一些指令,比如#include、#define等,将宏展开,处理条件编译等操作。
-
编译(Compile): 预处理完成后,源代码会被编译器翻译成机器能够执行的目标代码(通常是机器码或者中间代码)。编译器会检查代码的语法和语义,生成可执行文件。
-
链接(Link): 如果程序中包含了其他库函数或者模块,编译器会将这些模块连接到程序中,生成最终的可执行文件。链接器会解析符号引用,将不同模块之间的引用关系解决。
-
加载(Load): 可执行文件被加载到内存中,操作系统会为程序分配内存空间,并将程序加载到内存中准备执行。
-
执行(Execute): 程序开始在计算机上执行,按照代码的逻辑顺序执行各个语句,直至程序运行结束。
在Windows的C程序中实现进程间通信
-
管道(Pipes):管道是一种常见的进程间通信机制,可以在父子进程或者兄弟进程之间进行通信。在Windows中,可以使用匿名管道或命名管道。
-
共享内存(Shared Memory):通过共享内存,不同进程可以访问同一块内存区域,实现数据共享。
-
信号量(Semaphores):信号量用于控制多个进程对共享资源的访问,可以通过信号量来同步进程的操作。
-
消息队列(Message Queues):消息队列可以用来在进程之间传递消息,实现进程间通信。
-
套接字(Sockets):套接字通常用于网络编程,但也可以在同一台计算机的不同进程之间进行通信。
//管道方法
//父进程
#include <windows.h>
#include <stdio.h>
#define BUFFER_SIZE 25
int main()
{
HANDLE ReadHandle, WriteHandle;
STARTUPINFO si;
PROCESS_INFORMATION pi;
SECURITY_ATTRIBUTES sa;
char message[BUFFER_SIZE] = "Hello from parent process!";
DWORD written;
// 设置安全属性
sa.nLength = sizeof(SECURITY_ATTRIBUTES);
sa.bInheritHandle = TRUE;
sa.lpSecurityDescriptor = NULL;
// 创建管道
if (!CreatePipe(&ReadHandle, &WriteHandle, &sa, 0))
{
fprintf(stderr, "Create Pipe Failed");
return 1;
}
// 设置启动信息
ZeroMemory(&si, sizeof(si));
si.cb = sizeof(si);
si.hStdOutput = WriteHandle;
si.dwFlags = STARTF_USESTDHANDLES;
// 创建子进程
CreateProcess(NULL, "child_process.exe", NULL, NULL, TRUE, 0, NULL, NULL, &si, &pi);
// 父进程写入消息
WriteFile(WriteHandle, message, BUFFER_SIZE, &written, NULL);
printf("Parent wrote: %s\n", message);
// 等待子进程结束
WaitForSingleObject(pi.hProcess, INFINITE);
// 关闭句柄
CloseHandle(WriteHandle);
CloseHandle(ReadHandle);
CloseHandle(pi.hProcess);
CloseHandle(pi.hThread);
return 0;
}
//子进程
//子进程的可执行文件命名为child_process.exe
#include <windows.h>
#include <stdio.h>
#define BUFFER_SIZE 25
int main()
{
HANDLE ReadHandle;
STARTUPINFO si;
PROCESS_INFORMATION pi;
char message[BUFFER_SIZE];
DWORD read;
// 获取父进程传递的管道句柄
ReadHandle = GetStdHandle(STD_INPUT_HANDLE);
// 从管道中读取消息
ReadFile(ReadHandle, message, BUFFER_SIZE, &read, NULL);
printf("Child received: %s\n", message);
return 0;
}