c语言基础

字符数组和指针

  • 当你写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;
}


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值