C语法复习

数据类型

类型大小

数据类型大小数据类型大小
char1int4
short2long int8
float4double8
long long8long double16

关键字

register

声明局部变量,通知编译器将该变量储存在寄存器中(通常是内存),可以提高访问速度。一般用于某个局部变量会被多次访问的情况。

auto

声明局部变量,默认省略。在C语言中,只使用auto修饰变量,则变量类型为int,C++中会自动推导数据类型。

restrict

用来修饰指针,表明指针是访问一个数据对象的唯一且初始的方式.即它告诉编译器,所有修改该指针所指向内存中内容的操作都必须通过该指针来修改,而不能通过其它途径(其它变量或指针)来修改。(是简易性的,强制再次使用指针对变量进行引用、修改,编译器不会报错,结果也和不用restrict修饰相同)

sizeof

sizeof 在代码进⾏编译的时候,就根据表达式的类型确定了,类型的常用,而表达式的执行却要在程序运行期间才能执行,在编译期间已经将sizeof处理掉了,所以在运行期间就不会执行表达式了。

int main()
{
	short s = 2;
    int b = 10;
    printf("%d\n", sizeof(s = b+1));
    printf("s = %d\n", s);
    retrun 0;
}

sizeof(数组名)会返回整个数组的大小,其余情况下,sizeof(指针)返回的是4/8。

static

static修饰局部变量可以改变变量的生命周期,生命周期的改变本质是改变了变量的存储类型。被static修饰的变量会存储在静态区,存储在静态区的变量和全局变量是一样的,只有程序运行结束才销毁。但是作用域不变。

static修饰的全局变量具有全局作用域,只初始化一次,它与全局变量的区别在于如果程序包含多个文件的话,它作用于定义它的文件里,不能作用到其它文件里,即被static关键字修饰过的变量具有文件作用域。(使得作用域仅限于被定义的文件中即,从变量定义到本文件结尾处,其他文件不论通过什么方式都不能访问)。注意:xxx.hxxx.hpp中定义的全局变量即使使用static修饰,其他文件也可以访问这些变量,只有生命和定义分离的xxx.hxxx.cpp这种写法中,定义在xxx.cpp中被static修饰的全局变量才不能被其他文件访问。

static修饰函数和修饰全局变量相同。

signed和unsigned

char类型的数据默认可能是signed char,也可能是unsigned char,由当前系统决定。

其他类型的数据,默认是signed xxx

字符串

printf()和strlen()

遇到'\0'就会停止。

指针

数组名的理解

只有在sizeof(数组名)&数组名的时候,数组名表示整个数组,其余情况数组名都表示数组的首元素地址

指针数组和数组指针

int *p arr[5];   // 指针数组
int (*p) arr[5]; // 数组指针

函数指针

// 定义变量
int (*padd) (int, int) = Add;
int (*padd) (int, int) = &Add;
// 变量类型
int (*) (iny, int)
// 使用
int sum = padd(x, y);
int sum = (*padd)(x, y);

字符函数

大小写判断和转换

判断

int islower(int c);
int isupper(int c);

转换

int tolower(int c);
int toupper(int c);

strlen的模拟实现

#include <string.h>
size_t strlen(const char* str);

统计字符串中\0之前的字符个数,函数返回值为size_t类型。

size_t my_strlen(const char* str)
{
    assert(str);
    char* cur = str;
    while(*cur != '\0')
    {
        ++cur;
    }
    return cur - str;
}

strcpy的模拟实现

#include <string.h>
char* strcpy(char* dest, const char* src);
char* strncpy(char* dest, const char* src, size_t num);

源字符串必须以\0结尾,拷贝时也会拷贝\0

目标字符串必须有足够的空间,而且目标字符串可以修改。

strncpy不会自动补充\0

如果源字符串的长度小于num,则拷贝完源字符串之后,在目标的后边追加0,直到num个。

// 参数的顺序
// 函数的返回值
// 函数的功能,停止条件
// assert
// const修饰指针
char* my_strcpy(char* dest, const char* src)
{
	assert(src != NULL && dest != NULL);
	char* ret = dest;
	while (*dest++ = *src++)
	{
		;
	}
	return ret;
}

strcat的模拟实现

#include <string.h>
char* strcat(char* dest, const char* src);
char* strncat(char* dest, const char* src, size_t num);

源字符串必须以\0结尾。

目标字符串必须也以\0结尾,不然不知道从哪里开始追加。

不能自己给自己追加。

strncat会自动追加\0

char* my_strcat(char* dest, const char* src)
{
	assert(dest != NULL && src != NULL);
	char* ret = dest;
	while (*dest)
		++dest;
	while (*dest++ = *src++)
		;
	return ret;
}

strcmp的模拟实现

#include <string.h>
int strcmp(const char* str1, const char* str2);
int strncmp(const char* str1, const char* str2, size_t num);

如果str1大于str2,则返回一个大于0的数。

如果str1等于str2,则返回一个0。

如果str1小于str2,则返回一个小于0的数。

int my_strcmp(const char* str1, const char* str2)
{
	assert(str1 != NULL && str2 != NULL);
	while (*str1 == *str2)
	{
		if (*str1 == '\0')
			return 0;
		++str1;
		++str2;
	}
	return *str1 - *str2;
}

strstr的模拟实现

#include <string.h>
char* strstr(const char* str1, const char* str2);

函数返回str2第一次在str1中出现的位置。

字符串的比较匹配不包含\0,以\0作为结束标识。

char* my_strstr2(const char* str1, const char* str2)
{
	assert(str1 != NULL && str2 != NULL);
	if (*str2 == '\0')
		return (char*)str1;
	while (*str1)
	{
		char* s1 = (char*)str1;
		char* s2 = (char*)str2;
		while (*s1 && *s2 && *s1 == *s2)
		{
			++s1;
			++s2;
		}
		if (*s2 == '\0')
			return (char*)str1;
		++str1;
	}
	return NULL;
}

strtok的使用

#include <string.h>
char* strtok(char* str, const char* sep);

strtok会找到str中的下一个标记,并在其后添加\0,返回指向这个标记的指针,同时还会将这个标记从str中删除.

原字符串的改动是切分符原位置均更改为 \0,所以内容都还在,可以通过逐个字符打印检验。

int main()
{
    char str[80] = "This is - www.runoob.com - website";
    const char s[2] = "-";
    char *token;

    /* 获取第一个子字符串 */
    token = strtok(str, s);

    /* 继续获取其他的子字符串 */
    while (token != NULL)
    {
        printf("%s\n", token);

        token = strtok(NULL, s);
    }
    printf("\n");
    for (int i = 0; i < 34;i++)
        printf("%c", str[i]);

    return (0);
}

内存函数

memcpy的模拟实现

#include <memory.h>
void* memcpy(void* dest, const void* src, size_t num);

memcpysrc的位置开始向后复制num个字节的数据到dest

函数在遇到\0时不会停止。

函数的返回值和str1相同,所以一般不使用返回值,也无需接收返回值。

如果destsrc有重叠,结果未定义。这种情况应该使用memmove

void* my_memcpy(void* dest, const void* src, size_t num)
{
	assert(dest != NULL && src != NULL);
	void* ret = dest;
	while (num--)
	{
		*(char*)dest = *(char*)src;
		dest = (char*)dest + 1;
		src = (char*)src + 1;
	}
	return ret;
}

memmove的模拟实现

#include <memory.h>
void* memmove(void* dest, const void* src, size_t num);

memcpy的区别在于:memmove处理的内存块是可以存在重叠的。

void* my_memmove(void* dest, const void* src, size_t num)
{
	void* ret = dest;
	assert(dest != NULL && src != NULL);
	if (dest <= src || dest > (char*)src + num)
	{
		while (num--)
		{
			*(char*)dest = *(char*)src;
			dest = (char*)dest + 1;
			src = (char*)src + 1;
		}
	}
	else {
		dest = (char*)dest + num - 1;
		src = (char*)src + num - 1;
		while (num--)
		{
			*(char*)dest = *(char*)src;
			dest = (char*)dest - 1;
			src = (char*)src - 1;
		}
	}
	return ret;
}

memset和memcmp的使用

void* memset(void* dest, int value, size_t num);
int memcmp(const void* str1, const void* str2, size_t num);

数据在内存中的存储

大小端

小端模式:数据的高位字节内容存储在高地址处,低位字节内容存储在低地址处。

大端模式:数据的高位字节内容存储在低地址处,低位字节内容存储在高地址处。

结构体

匿名结构体

typedef struct
{
    int data;
    Node* next;
}Node;

这样定义匿名结构体是错的,因为在typedef之前,创建结构体的时候,内部就已经使用了Node这个名称,是错误的。

内存对齐

  1. 结构体的第一个成员对齐到和结构体变量起始位置偏移量为0的地址处。

  2. 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。对齐数 = 编译器的默认对齐数 和 该变量自身的大小 的较小值

  3. 结构体总大小为最大对齐数(结构体中每个成员变量都有⼀个对齐数,所有对齐数中最大的)的整数倍。

  4. 如果嵌套了结构体的情况,嵌套的结构体成员对齐到自己的成员中最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体中成员的对齐数)的整数倍。

修改默认对齐数

#pragma pack(1); 修改对齐数为1
#pragma pack();  修改对齐数为默认对齐数

位段

  1. 位段的成员必须是intunsigned intsigned int ,在C99中位段成员的类型也可以选择其他类型。

  2. 位段的成员名后边有⼀个冒号和⼀个数字。

struct A
{
    int _a:2;
    int _b:5;
    int _c:10;
    int _d:30;
};

位段不具有跨平台性。

位段的几个成员共有同一个字节,这样有些成员的起始位置并不是某个字节的起始位置,那么这些位置处是没有地址的。内存中每个字节分配一个地址,一个字节内部的比特是没有地址的。 所以不能对位段的成员使用&操作符,这样就不能使用scanf直接给位段的成员输入值,只能是先输入放在⼀个变量中,然后赋值给位段的成员。

struct A
{
    int _a:2;
    int _b:5;
    int _c:10;
    int _d:30;
};

int main()
{
    int a, b;
    struct A sa;
    scanf("%d %d", &a, &b);
    sa._a = a;
    sa._b = b;
    return 0;
}

联合体

  1. 联合的大小至少是最大成员的大小。

  2. 当最大成员大小不是最大对齐数的整数倍的时候,就要对齐到最大对齐数的整数倍。

动态内存管理

void* malloc(size_t size);
  1. 开辟一块连续的空间,如果成功则返回空间首地址,如果失败返回NULL,所以必须检查返回值。
  2. 返回值类型是void*,所以接收返回值之前要做强制类型转换。
void free(void* ptr);
  1. 如果ptr指向的不是动态开辟的空间,结果是未定义的
  2. 如果ptr == NULL,则什么都不做
  3. 如果该空间已经被释放,会报错。
  4. 如果ptr指向的是动态开辟的空间的一部分,不是起始位置,会报错。
void* calloc(size_t num, size_t size);
  1. num个大小为size的元素开辟空间,并将所有空间初始化为0
  2. callocmalloc在功能上的区别只有一个:calloc会对空间做初始化。
void* realloc(void* ptr, size_t size);
  1. 函数调用有可能失败,失败返回NULL。所以不能直接用ptr接收该函数的返回值,因为如果开辟失败,ptr更新为NULL,那么原来ptr指向的的空间就会造成内存泄露。

柔性数组

定义

C99中,结构中的最后一个元素允许是未知大小的数组,这就叫做『柔性数组』成员。

struct A
{
	int _a;
    int arr[]; // 柔性数组成员
};

特点

  1. 结构中的柔性数组成员前面必须至少一个其他成员。
  2. sizeof返回的这种结构大小不包括柔性数组的内存。
  3. 包含柔性数组成员的结构用malloc()函数进行内存的动态分配,并且分配的内存应该大于结构的大小,以适应柔性数组的预期大小。

使用

#include <stdio.h>
#include <stdlib.h>
int main()
{
    int i = 0;
    A *p = (A*)malloc(sizeof(A)+100*sizeof(int));
    //业务处理...
    p->i = 100;
    for(i=0; i<100; i++)
    {
        p->a[i] = i;
    }
    free(p);
    return 0;
}

总结

柔性数组是可以被指针替换的。

struct A
{
    int i;
    int *p_a;
};

柔性数组比指针的优势在于:

  1. 方便内存释放。只需要释放结构体的空间即可,而指针的方案则需要先释放指针指向的空间,再释放结构体的空间。
  2. 有利于提高访问速度,减少内存碎片。(牵强)

文件操作

文件的打开和关闭

FILE* fopen(const char* filename, const char* mode);
int close(FILE* stream);

mode表示文件的打开模式,下面是文件的打开模式。

文件使用方式含义如果指定文件不存在
“r”(只读)为了输入数据,打开⼀个已经存在的文本文件出错
“w”(只写)为了输出数据,打开一个文本文件建立一个新的文件
“a”(追加)向文本文件尾添加数据建立一个新的文件
“rb”(只读)为了输入数据,打开一个二进制文件出错
“wb”(只写)为了输出数据,打开一个二进制文件建立一个新的文件
“ab”(追加)向⼀个二进制文件尾添加数据建立一个新的文件
“r+”(读写)为了读和写,打开⼀个文本文件出错
“w+”(读写)为了读和写,建立⼀个新的文件建立一个新的文件
“a+”(读写)打开一个文件,在文件尾进行读写建立一个新的文件
“rb+”(读写)为了读和写打开一个二进制文件出错
“wb+”(读写)为了读和写,新建一个新的二进制文件建立一个新的文件
“ab+”(读写)打开一个二进制文件,在文件尾进行读和写建立一个新的文件

文件的顺序读写

顺序读写函数介绍

函数名功能适用于
fgetc字符输入函数所有输入流
fputc字符输出函数所有输出流
fgets文本行输入函数所有输入流
fputs文本行输出函数所有输出流
fscanf格式化输入函数所有输入流
fprintf格式化输出函数所有输出流
fread二进制输入文件输入流
fwrite二进制输出文件输出流

文件的随机读写

fseek

int fseek(FILE *stream, long int offset, int whence);

offset是相对于whence的偏移量,以字节为单位,正值表示向后,负值表示向前。

whence表示开始添加偏移offset的位置。它一般指定为下列常量之一:

选项含义
SEEK_SET文件的开头
SEEK_CUR文件指针的当前位置
SEEK_END文件的末尾

ftell

long int ftell(FILE *stream);

返回位置标识符的当前值。如果发生错误,则返回 -1,全局变量errno被设置为一个正值。

经常配合fseek来获取文件的大小:

#include <stdio.h>

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

   fp = fopen("file.txt", "r");
   if( fp == NULL ) 
   {
      perror ("打开文件错误");
      return(-1);
   }
   fseek(fp, 0, SEEK_END);

   len = ftell(fp);
   fclose(fp);

   printf("file.txt 的总大小 = %d 字节\n", len);
   
   return(0);
}

rewind

void rewind(FILE* stream);

让文件指针的位置回到文件的起始位置。

预处理、编译、汇编、链接

预处理

  1. 宏替换
  2. 处理条件编译
  3. 头文件展开
  4. 删除注释,注释被替换为一个空格
  5. 添加行号和文件名标识,方便后续编译器生成调试信息等。
  6. 或保留所有的#pragma的编译器指令,编译器后续会使用。

编译

编译过程就是将预处理后的文件进行一系列的:词法分析、语法分析、语义分析及优化,生成相应的汇编代码文件。

汇编

汇编器是将汇编代码转转变成机器可执行的指令,每一个汇编语句几乎都对应一条机器指令。就是根据汇编指令和机器指令的对照表一一的进行翻译,也不做指令优化。

链接

链接过程主要包括:地址和空间分配,符号决议和重定位等步骤。

预处理详解

预定义符号

__FILE__    // 进行编译的原文件
__LINE__    // 文件当前的行号
__DATE__    // 文件进行编译的日期
__TIME__    // 文件进行编译的时间
__STDC__    // 如果编译器遵循ANSI C,其值为1,否则未定义

宏函数

#define name( parament-list ) stuff

其中的parament-list是一个由逗号隔开的表达式,它们可能出现在stuff中。

参数列表的左括号必须与name紧邻,如果两者之间有任何空白存在,参数列表就会被解释为stuff的一部分。

用于对数值表达式进行求值的宏定义都应该加上括号,避免在使用宏时由于参数中的操作符或邻近操作符之间不可预料的相互作用。

宏替换的规则

  1. 在调用宏时,首先对参数进行检查,看看是否包含任何由#define定义的符号。如果是,它们首先被替换。
  2. 替换问本随后被插⼊到程序中原来文本的位置。对于宏,参数名被他们的值所替换。
  3. 最后,再次对结果文件进行扫描,看看它是否包含任何由#define定义的符号。如果是,就重复上述处理过程。

宏和函数的对比

属性#define定义宏函数
代码长度宏代码会插入到程序中,多次使用会导致代码长度增加函数代码只会存在一份
执行速度有创建、销毁栈帧的开销,相对慢一些
操作符优先级仅仅做简单的替换,不确定优先级函数优先计算出结果
带有副作用的参数(自增、自减)结果不可预测结果可预测
参数类型不关心关心
调试不可调试可调式
递归不支持递归支持递归

宏有时候可以做函数做不到的事情。比如:宏的参数可以出现类型,但是函数做不到。

#define MALLOC(num, type) (type*)malloc(num, sizeof(type))

int main()
{
    // 使用时可以以类型做参数
    int* p = MALLOC(10, int);
    // 预处理替换之后
    int* p = (int*)malloc(10, sizeof(int));
    return 0;
}

#和##

#运算符将宏的⼀个参数转换为字符串字面量。它仅允许出现在带参数的宏的替换列表中。#运算符所执行的操作可以理解为”字符串化“。

#define PRINT(n) printf("The value of "#n" is %d\n", n);

这个宏在使用时,会把#n替换为"n"

int x = 10;
PRINT(x); // 宏替换为 printf("The value of ""x" is %d\n", n);
// 输出 The value of x is 10

##可以把位于它两边的符号合成一个符号,它允许宏定义从分离的文本片段创建标识符。##被称为记号粘合。

比如,我们想快速实现以下函数

int int_max(int x, int y)
{
    return x>y?x:y;
}

float float_max(float x, float y)
{
    return x>yx:y;
}

...

我们可以使用宏替换

// 宏定义
#define GENERIC_MAX(type)      \
type type##_max(type x, type y)\
{                              \
	return (x > y ? x : y);          \
}

// 使用宏快速定义函数
GENERIC_MAX(int)
GENERIC_MAX(float)
...
    
// 使用函数
int main()
{
    int x = 0;
    int y = 1;
    int max1 = int_max(x, y);
    float a = 1.0;
    float b = 2.5;
    float max2 = float_max(a, b);
    printf("%d %f\n", max1, max2);
    return 0;
}

undef

#undef NAME // 移除NAME的定义

命令行定义

// Linux环境
gcc -D SIZE=10 main.c
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值