1、实用C笔记

C笔记

1、基础知识------------------------

1、基础中的基础

ASCLL表

常见ASCII码的大小规则:09<AZ<a~z
“A”为65;“a”为97;“0”为 48。
在这里插入图片描述

0.0十进制转二进制的原理

十进制整数转二进制
假设十进制整数值为A,对应的二进制数为abcde (每一位的值非0即1)。
众所周知,二进制数换算为十进制的公式如下:
A = a * 2^4 + b * 2^3 + c * 2^2 + d * 2^1 + e * 2^0
所以咱们只要把a,b,c,d,e都取出来就能得到二进制的值。
又 A = 2 * (a * 23 + b * 22 + c * 21 + d * 20) + e
所以A除以2即能得到整数位(a * 23 + b * 22 + c * 21 + d * 20) 和余数e
上边得到的整数(a * 23 + b * 22 + c * 21 + d * 20)除以2即能得到整数位(a * 22 + b * 21 + c * 20)和余数d。
以此类推,可得c,b,a
在这里插入图片描述

//1、判断i转为二进制的1的个数
cnt=0;
while(i)
 {
  cnt++;
  i=i&(i-1);
 }
 return cnt;
}
//2、判断n是不是2的次幂
(n > 0 && ((n & (n - 1)) == 0)//是为真;

0.1、运算符

|| 和 &&:逻辑运算符
| 和 &:位运算符
|| 或操作,|| 为界将表达式分为两部分,他会先算前一部分,如果前一部分为真,他将停止运算,如果为假,他才会算第二部分
&&且操作 ,&&为界将表达式分为两部分,他会先算前一部分,如果前一部分为假,他将停止运算,如果为真,他才会算第二部分

0.2、“&= ^= |= >>=”

C语言中“&=,^=,|=”分别表示什么意思
1、C语言中的 >>= 意思为:右移后赋值

代码示例为:

x = 8;

x >>= 3;

右移后结果为:00000000 00000000 00000000 00000001

在这里插入图片描述

2、C语言中的 <<= 意思为:左移后赋值

代码示例为:

x = 8;

x <<= 3;

左移后赋值结果为:00000000 00000000 00000000 01000000

在这里插入图片描述

3、C语言中的 &= 意思为:按位与后赋值

代码示例为:

x = 0x02;

x &= 0x01;

按位与后的结果为:0x00
在这里插入图片描述

4、C语言中的 ^= 意思为:按位异或后赋值

代码示例为:

x = 0x02;

x ^= 0x01;

按位异或的结果为:0x03 等同于0011

在这里插入图片描述

5、C语言中的 |= 意思为:按位或后赋值

代码示例为:

x = 0x02;      //二进制10

x  |= 0x01;    //二进制01

相当于:x = x | 01 = 10 | 01
按位或的结果为:0x03 等同于0011

扩展资料:

0.2.1、C语言运算符的优先级顺序

运算符的优先级从高到低大致是:单目运算符、算术运算符、关系运算符、逻辑运算符、条件运算符、赋值运算符(=)和逗号运算符。
参考SCDN
在这里插入图片描述
基本的优先级需要记住:

指针最优,单目运算优于双目运算。如正负号。

先乘除(模),后加减。

先算术运算,后移位运算,最后位运算。

请特别注意:1 << 3 + 2 & 7等价于 (1 << (3 + 2))&7.

逻辑运算最后计算。

C语言常用运算符的优先级口诀是:“单算关逻条赋逗”
如果加入位运算符,完整口诀是:“单算移关与,异或逻条赋”
■“单”表示单目运算符:逻辑非(!),按位取反(~),自增(++),自减(–),取地址(&),取值();
■“算”表示算术运算符:乘、除和求余(
,/,%)级别高于加减(+,-);
■“移”表示按位左移(<<)和位右移(>>);
■“关”表示关系运算符:大小关系(>,>=,<,<=)级别高于相等不相等关系(==,!=);
■“与”表示按位与(&);
■“异”表示按位异或(^);
■“或”表示按位或(|);
■“逻”表示逻辑运算符:逻辑与(&&)级别高于逻辑或(||);
■“条”表示条件运算符(? 😃;
■“赋”表示赋值运算符(=,+=,-=,*=,/=,%=,>>=,<<=,&=,^=, |=,!=);
◆另,逗号运算符(,) 级别最低,口诀中没有表述,需另加记忆…

0.3、c语言中!与~

!: 代表值得取反,对于整形变量,只要不为0,使用 ! 取反都是0,0取反就是1。就像 bool 只有真假一样。

~: 代表位的取反,对于整形变量,对每一个二进制位进行取反,0变1,1变0。

0.4、c语言\n和\r

c语言里:
反斜杠 \r 代表回车
反斜杠 \n代表换行

回车 不等于 换行,回车是到当前行的首位置,换行是到下一行的首位置。
可以用\r刷新当前行的内容,可以用\n开始输入下一喊的内容

0.4、‘\0’

  • ‘\0’ 是转义字符

为了告诉编译器’\0’是空字符,而不是字符0.

在c语言中通常用一个字符数组来存放字符串,’\0’ 是字符串的结束标志,任何字符串之后都会自动加上’\0’。如果字符串末尾少了‘\0’转义字符,则在输出时可能会出现乱码问题。
接下来结合代码来看
1.不指定字符串数组长度时
正确赋值

char str1[] = {"abc"};
//字符数组str在内存中的实际存放情况为:a b c '\0'
//后面的'\0'是编译器自动加上的,所以在给字符串数组赋初值的时候不用指定数组的长度.

错误的赋值

char str[] = {'a','b','c'};
printf("%s\n",str);
//用单引号赋值时,会丢失'\0',所以此时字符串数组无结束标志,打印时后面会出现乱码

2.指定字符串数组长度时
由于c语言中以’\0’作为字符串数组的结束标志,虽然’\0’不计入串长,但是要占用内存空间一个字节,所以要留出一个字节存储’\0’

注意:
1.当字符串数组中间出现’\0’时,’\0’后面的字符不再打印,因为’\0’为字符串数组的结束标志编译器读取到’\0’时会认为这个字符串数组已经结束。

char str1[] = {"abc\0abc"};
char str2[] = {"abc"};
//str1与str2打印的内容一样

0.5、宏定义 do{}while(0)

0、小例子:宏定义MIN: #define MIN(a,b) ((a<=b)?(a):(b))
1、#define “宏定义” do{“若干条语句”}while(0)

用于将打括号中的若干条语句形成一个整体,防止用宏定义时出错

参考:
(4)以上的第3种情况用单独的{}也可以实现,但是为什么一定要一个do{}while(0)呢,看以下代码:

//宏定义
 #define switch(x,y) {int tmp; tmp="x";x=y;y=tmp;}
 //正文引用
      if(x>y)
        switch(x,y);
      else       //error, parse error before else
      otheraction();

在把宏引入代码中,会多出一个分号,从而会报错。这对这一点,可以将if和else语句用{}括起来,可以避免分号错误。
使用do{….}while(0) 把它包裹起来,成为一个独立的语法单元,从而不会与上下文发生混淆。同时因为绝大多数的编译器都能够识别do{…}while(0)这种无用的循环并进行优化,所以使用这种方法也不会导致程序的性能降低

0.5、正文 do{}while(0)

do{
	if(){
		....
		break;
	}
	do{
	if(){
		....
		break;
	}
while(0);  //或者使用while(false)

这段代码使用了一个 do-while 循环,尽管看起来像一个循环结构,但其实意图是创建一个有条件的代码块。

这种方式通常被称为 “do-while false” 技巧或 “do-while 0” 技巧,这是一种将一组语句封装在一个有条件执行的代码块中的方法。尽管看起来像是一个循环,但实际上只会执行一次,然后结束。

这种技巧的目的是使得可以使用 break 语句来退出整个代码块,类似于在函数中使用 return。这种结构有助于代码的可读性和整洁性,使得可以通过 break 直接跳出该代码块,并且可以在需要时进行清理操作。

0.5-符号常量

  • 在C语言中,可以用一个标识符来表示一个常量,称之为符号常量。其特点是编译后写在代码区,不可寻址,不可更改,属于指令的一部分。
1. 符号常量定义用#define:

符号常量在使用之前必须先定义,其一般形式为:

形式为:#define 标识符 常量

eg: #define PI 3.14 //没有分号

#define 是一条预编译命令(预处理命令都以"#"开头),称为宏定义命令,在预编译时仅仅是进行字符替换。符号常量不占内存,只是一个临时符号,在预编译后这个符号就不存在了,故不能对符号常量赋以新值。习惯上符号常量的标识符用大写字母,变量标识符用小写字母,以示区别。

2. 常变量定义const

形式为 :const type name = value;

eg:const int N= 12;

这样就可以在程序中使用N而不是12了。常量(如N)被初始化后,其值就被固定了,编译器将不允许再修改该常量的值。此外注意应在声明中对const进行初始化。这种形式叫常变量,分配有存储空间,但是其中的内容不允许改变。常变量具有符号常量的优点,而且使用方便,有了常变量以后,可以不必多使用符号常量。

3. enum

形式为:enum EnumName{标识符,标识符,…};

eg: enum money{ONE,TWO,FIVE=5,TEN=10};

枚举类型中的元素也是符号常量。
————————————————
参考:「浪迹天涯_」https://blog.csdn.net/wo17fang/article/details/52662146

0.6 #if #else #endif

条件编译
#if 表达式
程序段1
#else
程序段2
#endif
2.功能:当表达式为非0(“逻辑真”)时,编译程序段1,否则编译程序段2。

[案例] 输入一个口令,根据需要设置条件编译,使之能将口令原码输出,或仅输出若干星号"*"#define PASSWORD 0 /预置为输出星号/
main()
{ …… /条件编译/
#if PASSWORD /源码输出/
……
#else /输出星号/
……
#endif
…… }

0.7#ifdef #ifndef #endif

条件编译
条件编译命令最常见的形式为:

 #ifdef 标识符
    程序段1
    #else
    程序段2
    #endif
    // 它的作用是:当标识符已经被定义过(一般是用#define命令定义),则对程序段1进行编译,否则编译程序段2。
其中#else部分也可以没有,即:
 #ifdef
    程序段1
    #denif

这里的“程序段”可以是语句组,也可以是命令行。这种条件编译可以提高C源程序的通用性。如果一个C源程序在不同计算机系统上系统上运行,而不同的计算机 又有一定的差异。例如,我们有一个数据类型,在Windows平台中,应该使用long类型表示,而在其他平台应该使用float表示,这样往往需要对源 程序作必要的修改,这就降低了程序的通用性。可以用以下的条件编译:

#ifdef WINDOWS
    #define MYTYPE long
    #else
    #define MYTYPE float
    #endif

案例

//globe.h
#ifdef EXTERN
    extern char globle_str[];
#else
    int i=11;
#endif

//main.c
#define EXTERN    //定义宏EXTERN后
#include "globe.h" 
#include <stdio.h>
void output()
{
    puts(globe_str);//上边定义了宏EXTERN,所以这里可以用globe.h中的char globle_str[]变量
}

//main_1.c
#include "globe.h" 
void output();
int main()
{
    printf("%d",i);//上边没有定义宏EXTERN,所以这里不能用globe.h中的char globle_str[]变量
    output();
    return 0;
}

0.8多个.c引用一个.h

头件的中的#ifndef,这是一个很关键的东西。比如你有两个C文件,这两个C文件都include了同一个头文件。而编译时,这两个C文件要一同编译成一个可运行文件,于是问题来了,大量的声明冲突。

还是把头文件的内容都放在#ifndef和#endif中吧。不管你的头文件会不会被多个文件引用,你都要加上这个。一般格式是这样的:

#ifndef <标识>
#define <标识>


#endif

<标识>在理论上来说可以是自由命名的,但每个头文件的这个“标识”都应该是唯一的。标识的命名规则一般是头文件名全大写,前后加下划线,并把文件名中的“.”也变成下划线,如:stdio.h

#ifndef STDIO_H
#define STDIO_H

#endif
例如STM32跑马灯实验中led.h文件:

#ifndef __LED_H
#define __LED_H
#include "sys.h"
//LED 端口定义
#define LED0 PBout(5)// DS0
#define LED1 PEout(5)// DS1
void LED_Init(void);//初始化 
#endif

0.9 原码 / 补码 / 反码 / 移码

移码
  1. 原码、反码、补码和移码参考CSDN

例题:若9BH表示移码(含1位符号位).其对应的十进制数是
答:9BH是用十六进制表示的,它的二进制表示为10011011 移码是符号位取反的补码,则补码为00011011,正数的补码与原码相同, 则原码为00011011 表示为十进制为27。

1. 原码

原码就是符号位加上真值的绝对值, 即用第一位表示符号, 其余位表示值. 比如如果是8位二进制:

[+1]= 0000 0001
[-1]= 1000 0001
2. 反码

反码的表示方法是:

正数的反码是其本身

负数的反码是在其原码的基础上, 符号位不变,其余各个位取反.

[+1] = [00000001]= [00000001][-1] = [10000001]= [11111110]
3. 补码

补码的表示方法是:

正数的补码就是其本身

负数的补码是在其原码的基础上, 符号位不变, 其余各位取反, 最后+1. (即在反码的基础上+1)

[+1] = [00000001]= [00000001]= [00000001][-1] = [10000001]= [11111110]= [11111111]
4. 移码

移码最简单了,不管正负数,只要将其补码的符号位取反即可。

例如:X=-101011 , [X]= 10101011[X]=11010100[X]=11010101[X]=01010101

为何要使用原码, 反码和补码?
为了让计算机计算减法更方便(计算机只能做加法运算)
1-1 可以转化为-> 1+(-1)硬件内实际进将负数的反码和其他数进行与运算

数据类型专题——————

0. 基本数据类型(及大小)

参考:C语言 中的 数据类型 超详解
在这里插入图片描述
在这里插入图片描述

1. int / long int / long long

  1. int、long int 和 long long int 的取值范围
    我们在做整型数字运算时,经常会遇到由于数字值极大导致的结果溢出,导致我们得到错误的结果,大多数情况下将变量定义为 long long int 即可。

为了便于理解,我们需要知道int、long int、long long int各自能够表示什么范围:

int -2147483648~2147483647 //相当于0111 1111 1111 1111 1111 1111 1111 1111 (31位的1加上一个符号位)
long int -2147483648~2147483647 //原理同上
long long int -9223372036854775808~9223372036854775807

那么我们就明白了,相应的无符号类型的各自表示范围为:

unsigned int 0~4294967295 //无符号位,直接32位符号位
unsigned long int 0~4294967295
unsigned long long int 0~18446744073709551615

  1. 三者的打印
    其实在C语言的 limits.h 库文件中已经包含了对应类型的极大和极小的整数值,直接调用就可以读取,例如:
#include<stdio.h>
#include <limits.h>

int intmax = INT_MAX;
int intmin = INT_MIN;
long long int longlongmax = LLONG_MAX;    //用%lld转化
long long int longlongmin = LLONG_MIN;

int main()
{
printf(“
intmax = %d\
nintmin =  %d\
nlonglongmax = %lld\
nlonglongmin = %lld\
n”, intmax, intmin, longlongmax, longlongmin);
return 0;
}

在这里插入图片描述

2. 类型转换原理(数值的变化)

char型数组强转成int型

例:
4个char数组{0, 0, 0x01, 0x01},强制转换为1个int型整数
比如char数组,{00000000,00000000,00000001,00000001}
转换为int后就是,00000000 00000000 00000001 00000001 = 257 //将二进制拼接然后转化为十进制

  • 自创大小端序检测器
#include <stdio.h>
	//手撸大小端序检测器
	int main(){
	char a[] = {0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08};
	int *p = (int*)a;
	printf("%x\n",*++p); 
   return 0;
}

输出:8070605(小端序)

2、编译、C文件结构相关

1.C文件结构

  • C语言函数的定义和声明
    参考CSDN
  1. 只有后面定义的函数才可以调用前面定义过的函数
  2. 如果想把函数的定义写在main函数后面,而且main函数能正常调用这些函数,那就必须在main函数的前面进行函数的声明

2.预处理

参考CSDN
预编译的主要作用如下:

将源文件中以”include”格式包含的文件复制到编译的源文件中。
用实际值替换用“#define”定义的字符串。
根据“#if”后面的条件决定需要编译的代码。

预处理指令

大多数预处理器指令属于下面3种类型:

宏定义:#define 指令定义一个宏,#undef指令删除一个宏定义。
文件包含:#include指令导致一个指定文件的内容被包含到程序中。
条件编译:#if,#ifdef,#ifndef,#elif,#else和#endif指令可以根据编译器可以测试的条件来将一段文本包含到程序中或排除在程序之外。

可以理解为预处理(预编译)时进行以上三部分的展开或者替代

3.条件编译

参考CSDN

3、C语言关键字

0、C关键字

由ASCII标准定义的C语言关键字共32个

  1. 数据关键字12个:char,double,float,enum,float,int,long,short,signed,struct,union,void

  2. 控制语句关键字12个:for,do,while,break,continue,if,else,goto,switch,case,default,return

  3. 存储类型关键字4个:auto,extern,regsiter,static

  4. 其他关键字4个:const,sizeof,typedef,volatile
    1、C语言关键字

面试问过的关键字——————

1. static

  1. 在函数体,被声明为静态变量的变量在这一函数被调用过程中维持其值不变;

  2. 在模块内(但在函数体外),被声明为静态变量的变量可以被模块内所有函数访问,但不能被模块外其他函数访问,它是一个本地的全局变量;

  3. 函数用static修饰,改变了作用域。普通的函数是可以通过头文件声名的方式被其他文件调用,被static修饰后就只能在本文件里被调用,这样是为了数据的安全。

  4. 局部变量:
    当用static修饰后,局部变量的生存周期就是当程序结束才会结束。
    用static修饰的局部变量,改变了生存周期,但是没有改变其作用域改变其生存周期的原因是被static修饰的局部变量被存放在.bss段或者.data段,而普通的局部变量是存放在栈上的。

  5. 全局变量:
    全局变量用static修饰改变了作用域,没有改变生存周期
    一旦被static修饰,就只能被定义该全局变量的.c文件引用

  6. 函数:
    改变了作用域,没有改变其生存周期

2. const:

  1. 只读
  2. 使编辑器保护那些不希望被改变的参数,防止其被无意的代码修改;
  3. 因为常量在定义以后就不能被修改,所以使用const定义变量时必须初始化。

3. volatile

开发者告诉编译器该变量是易变的,无非就是希望编译器去注意该变量的状态,时刻注意该变量是易变的,每次读取该变量的值都重新从内存中读取

4. auto

auto 是 C++11 引入的关键字,用于声明变量时由编译器自动推断其类型。通过 auto,编译器可以根据变量的初始化表达式来确定变量的类型,这使得代码编写更加简洁和易读。

auto num = 10; // 推断 num 的类型为 int
auto name = "John"; // 推断 name 的类型为 const char*
auto result = calculateResult(); // 推断 result 的类型根据 calculateResult() 的返回类型

在这些例子中,auto 关键字允许编译器根据右侧的初始化表达式来推断变量的类型。这使得代码更加灵活,尤其是在复杂类型或模板类型的场景中,可以避免显式指定类型,简化代码书写。

使用 auto 关键字需要注意以下几点:

类型推断: auto 关键字通过初始化表达式来推断变量的类型。如果初始化表达式的类型是引用类型,则 auto 推断出的变量也会是引用类型。

初始化表达式: auto 变量必须在声明时进行初始化,编译器需要根据初始化表达式来推断变量类型。

代码可读性: 虽然 auto 可以减少代码中的类型声明,但有时过度使用 auto 可能会降低代码的可读性。在一些情况下,显式指定类型可能更有利于代码的可维护性和可读性。

总之,auto 关键字是 C++ 中的一个强大特性,可以简化代码并提高灵活性,但在使用时需要权衡可读性和便利性。

冷门关键词/宏——————

1. assert(断言)宏

assert宏的原型定义在<assert.h>中,其作用是如果它的条件返回错误,则终止程序执行,原型定义:

#include <assert.h>
void assert(int expression);

assert的作用是现计算表达式 expression ,如果其值为假(即为0),那么它先向stderr打印一条出错信息,然后通过调用 abort 来终止程序运行。请看下面的程序清单badptr.c:

#include <stdio.h>
#include <assert.h>
#include <stdlib.h>
int main( void )
{
       FILE *fp;
    
       fp = fopen( "test.txt", "w" );//以可写的方式打开一个文件,如果不存在就创建一个同名文件
       assert( fp );                 //所以这里不会出错
       fclose( fp );
    
       fp = fopen( "noexistfile.txt", "r" );//以只读的方式打开一个文件,如果不存在就打开文件失败
       assert( fp );                       //所以这里出错
       fclose( fp );                       //程序永远都执行不到这里来
       return 0;
}

2. inline内联函数

static inline

内联函数(inline)可以减少CPU的系统开销,并且程序的整体速度将加快,但当内联函数很大时,会有相反的作用,因此一般比较小的函数才使用内联函数。
通常,程序执行时,处理器从内存中读取代码执行。当程序中调用一个函数时,程序跳到存储器中保存函数的位置,开始读取代码执行,执行完后再返回。
为了提高速度,C语言定义了inline函数,告诉编译器把函数代码在编译时直接拷贝到程序中,这样就不用执行时另外读取函数代码。
static函数告诉编译器其他文件看不到这个函数,因此该函数只能在当前文件中被调用。static inline函数只能在当前文件中被调用,同时执行速度快,几个文件中都可以使用同样的函数名。

示例如下:

void  main(){
     int  i;
     for (i=0;i<100;i++){
         myprint();
     }
}
void  myprint(){
     printf ( "ok" );
}

上述代码中,在主函数中调用100次myprint函数,意味着这个函数要进栈100次,出栈100次,这样一来我们就打印一个小小的ok就把大量的时间花费在进栈和出栈之上了。我们可以把代码改为:

void  main()
{
     int  i;
     for (i=0;i<100;i++)
     {
         myprint();
     }
}
static  inline  void  myprint()
{
     printf ( "ok" );
}
此时相当于:
void  main()
{
     int  i;
     for (i=0;i<100;i++)
     {
         printf ( "ok" );
     }
}

GCC会在其调用内联函数处将其汇编展开编译,而不为这个函数生成独立的汇编编码。以下几种情况除外:

  1. 函数的地址被使用的时候。如通过函数指针对函数进行了间接调用。这种情况下就不得不为static inline函数生成独立的汇编码,否则它没有自己的地址。

  2. 函数本身有递归调用自身的行为。
    采用内联函数其实是以空间换取时间的做法。

4、C语言的组成

1.数组

0. 一维数组空间问题
  • int a[5]; //栈区分配空间
  • int* b = (int*)malloc(sizeof(int)*5); //堆区分配空间
#include <stdio.h>
#include <stdlib.h>

int main(void) { 
    int a[5];
	int* b = (int*)malloc(sizeof(int)*5);

    printf("a的空间 = %d\n b的空间 = %d\n", sizeof(a),sizeof(b));

	return 0;
}

在这里插入图片描述

1.二维数组

一般的我们说的C/C++中的二维数组是定义在栈中的二维数组。比如定义了一个array[3][4],那就指的是定义了一个三行四列的矩阵形状的二维数组,如下图所示。这样的矩阵在内存中是以箭头右边的方式存放的,也就是说实际上我们定义的二维数组在内存中仍然像是一维数组那样连续存储的
在这里插入图片描述

/* 可以使用下面的方法来初始化二维数组
 * 也就是直接将二维数组铺平,写成一维数组的形式
 */
int array[3][4] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12};
 
/* 可以使用标准的二维数组的初始化方式
 */
int array[3][4] = {{1, 2, 3, 4}, {5, 6, 7, 8}, {9, 10, 11, 12}};
2. 二维数组申请空间

参考CSDN
方法一:一般方式

#include<stdio.h>
#include<string.h>
#include<stdlib.h>
int main()
{
     int **data,i,j;
     int row=5,column=10;//申请一个五行十列的int行的二维数组
     data=(int **)malloc(sizeof(int *)*row); //在二维数组内申请row个一维数组
     for(i=0;i<row;i++)
     {
         data[i]=(int *)malloc(sizeof(int)*column);  //逐个为这些一维数组申请column个int型空间
         memset(data[i],0,sizeof(int)*column);		//并将这些一维数组初始化为0
     }

	//打印
     for(i=0;i<row;i++)  //打印这个二维数组
     {
         for(j=0;j<column;j++)
            printf("  %d  ",data[i][j]);
         printf("\n");
     }
     free(data); //释放这个二维数组
}

方法二:用数组指针实现
(一步到位)

int main()
{
	//数组指针,指向int[4]的一个指针
	int(*p)[4] = (int(*) [4])malloc(sizeof(int[4]) * 3);//Malloc前面的是强制类型转化->三行四列
	//一步到位
	if (p == NULL)
		return NULL;
	for (int i = 0; i < 3; i++)
	{
		for (int j = 0; j < 4; j++)
		{
			printf("%d\t", p[i][j] = i + j);
		}
		printf("\n");
	}
	return 0;
}
3.字符串数组
0. 字符串数组和整型数组相互转换

C语言—— 字符串 和 数组 之间的转换

  1. 数组 —> 字符串
    利用ASCII方式进行转换
#include <stdio.h> 
#include <string.h> 

int main()
{
	int i;
	int arry[7] = {5,2,0,1,3,1,4};
	char str1[10] = "\0";
	
	for(i=0;i<7;i++)
	{
		str1[i] = arry[i] + '0';
	}
	str1[i] = '\0';   //不可省略 
	
	printf("%s\r\n",str1);
	
	return 0;
}

  1. 字符串 —> 数组
#include <stdio.h> 
#include <string.h> 

int main()
{
	int i;
	int arry[10];
	char str1[ ] = "5201314";
	
	for(i=0; i<sizeof(str1)-1; i++)
	{
		arry[i] = str1[i] - '0';
		printf("arry[%d]=%d\r\n", i, arry[i]);
	}
	
	return 0;
}

1.一维字符串数组
/*数组声明后初始化为零*/
int a[10] = {0};
char b[10] = {0};
/*正确的声明以及定义*/
char* a = "aabbcc";
char a[] = {"aa" "bb" "cc"};
char a[10] = {"aa" "bb" "cc"};
char a[10] = {"asdfghjkl"};   //以后就这么写了!
char a[] = {'a','b','c'};
char a[] = {'a','b','c','\0'};
char a[10] = {'a','b','c','\0'};

/*错误的声明以及定义*/
char* a = "aabbcc";
char* b[10];
b = a;  //指针不能给数组赋值
//“数组声明后地址已经确定,不能再由指针或数组再赋给该数组新的地址”
//可以使用strcpy(b,a);
//可以找个指针,把这些数据先赋值给指针
char a[] = {'a' 'b' 'c' '\0'};
  • 对于16进制
char a[4] = {0x05,0xc5,0xb3,0xea};
int d[] = {0x10,0x04};
printf("%x\n",d[0]);
printf("%d\n",d[0]);
//输出:
10
16
2.(二维)字符串数组
#include <stdio.h>

int main()
{
	char *s[] = {"abc", "1234", "56789"}; //二维数组
	char* p = s[1];
	char b[] = {"aa" "bb" "cc"};
	char c[] = {"abc" "1234" "56789"}; //没有逗号就是一维数组
	
    printf("c[] = %s\n", c);
	printf("b[] = %s\n", b);
    printf("*(p+1) = %s\n", (p+1));
	printf("s[0] = %s\n", s[0]);
	
   
   return 0;
}

在这里插入图片描述
3. 二维字符串数组的问题

    char a[2][10];
    a[0] = "shhajsa";

这是错误的
原因:

(1)数组不能直接给数组赋值
(2)指针不能直接给数组赋值

//前提条件
char a[] = {'a','b','c'};
char b[3];

char  c[3]  = a;	//错误---》数组不能直接给数组赋值
char  d[3] 	= p;	//错误---》指针不能直接给数组赋值
char *p  = a;	//正确赋值,数组名为数组首元素地址,因此可使用指针保存该地址(指针只能保存地址)
strcpy(b,a);	//正确,使用标准库函数可实现字符串拷贝
char **p1 = &p;	//正确,二级指针可接收一级指针的地址

正确做法:

char a[2][10] = {"shhajsa","hhahjha"};
  1. 二维数组的另一个问题
int main( void ){
    static int a[10], b[10];
    int c[10], d[10];
    int* e[] = { a, b };     /* A */
    int* f[] = { c, d };     /* B */
    return 0;
}

B为什么不能通过编译?是由于自动数组名并不是常量表达式。在C中,常量表达式必须是编译期的,只在运行期不变的实体不是常量表达式,请看标准的摘录:

c和d是自动数组,首地址在编译期是不可知的,因为这样的对象在编译期还不存在a和b是静态数组,静态对象从程序开始时就已存在,因此a和b的首地址在编译期是已知的,它们都属于常量表达式中的地址常量表达式。

疑惑
  1. 指针不可以给一维数组赋值,但可以给未malloc一维数组的二维数组赋值
    解答:一维数组是不能直接进行赋值的,这是因为数组名是一个常量指针,不能被赋值。
    这样可以(未malloc一维数组的二维数组)
int main(void) {
    char *a = "asdf";
    char **b = (char**)malloc(sizeof(char*)*10);
    int i = 10;
    while(i--)
    {
    b[i] = a;
    printf("%s\n", b[i]);        
    }
    return 0;
}

这样也可以(malloc一维数组的二维数组)

int main(void) {
    char *a = "asdf";
    char **b = (char**)malloc(sizeof(char*)*10);
    int i = 10;
    while(i--)
    {
    b[i] = (char*)malloc(sizeof(char)*10);
    b[i] = a;
    printf("%s\n", b[i]);        
    }
    return 0;
}

2.指针

0、指针变量声明
  • 指针的书写格式:
    这里所说的“书写格式”,是指定义或声明指针的时候大家习惯的书写方式,大概常见的为以下几种:
int num = 1;
int *a = &num;	//格式一
int * b = &num;	//格式二
int* c = &num;	//格式三

格式三将符号与数据类型int放在一起,意味着把int看做一个整体,这实际上也是编译器实际工作的方式,将int作为一种独立的数据类型来看待,我们可以将其称作“整型指针”类型,注意这种数据类型表示“指针本身”的数据类型,而不是“指针指向的数据”的数据类型,这二者是完全不同的。同理,char,float*也一样。个人认为,这种表示方法更符合C/C++的编程思想,更容易理解。但是,这样表示对于同一行定义多个变量亦会引起误解,比如:

char* a, b, c;

上面的写法很可能并不是我们的本意,编译器仅仅会将a定义为char*类型的变量,而b和c均为char类型,而我们本来可能是想表示为:

char* a, *b, *c;
但这样写又失去了对称美。个人建议还是拆分成多行定义,

char* a;
char* b;
char* c;

这样不仅美观,而且易读易理解。

1、指针和引用
  1. 引用必须被初始化,指针不必。
  2. 引用(相当于指针常量)初始化以后不能被改变,指针可以改变所指的对象。
  3. 不存在指向空值的引用,但是存在指向空值的指针。
引用:引用是一个变量的另一个名字,又称别名。定义方式:
int a=10int &b=a;
在这里,意思就是给a变量起了一个新名字b,因此b不可再次被重新定义。
引用必须初始化,无空引用,并且引用不分等级。
引用与指针的异同点:
相同点:
在引用中 int &b=a;这一句代码可被编译器看做int * const b=&a;//加上const的作用是表明指针b的自身的值(a的地址)不能改变,而指向的值(a的值)可以改变。也就是说引用相当于指针常量,引用与指针在编译时的方法一样,指向的都是变量的内存地址。
不同点:
1.在定义方式中引用储存的是值,而指针是一个变量的拷贝,存储的是地址。
2.引用只能对已经存在的变量或对象实现引用,而指针则不需要,可以定义为空。
3.在函数的传参中,如果传递的是一个引用,意味着这个变量或对象已经存在了;如果传递的是一个指针,则不能判断这个指针是不是有效的,是不是空的,因此在函数体 中大多数都得进行指针是否为空的判断。但是虽然引用较为安全,但是利用指针来传参效率较快。
4.引用是直接访问变量,不用分配自己的内存空间,而指针是间接访问,需要定义,需要有自己的内存空间。

3. 结构体

0.结构体的声明
  1. 一般形式
关于结构体类型的定义的总结;
一般格式就是;
struct 结构体名(也就是可选标记名)
{
    成员变量;
}//使用分号表示定义结束;
  1. 省略可选标记名
其一;
struct
{
    char title[MAXTITL]; 
    char author[MAXAUTL];
    float value; 
}library;
//注意这里不再是定义声明结构体类型,而是直接创建结构体变量了,这个编译器会分配内存的;
//这样的确可以省略标识符也就是结构体名,但是只能使用一次;因为这是;声明结构体的过程和定义结构体变量的过程和在了一起;并且个成员变量没有初始化的;
//如果你想多次使用一个结构体模块,这样子是行不通的;
其二;
用typedef定义新类型名来代替已有类型名,即给已有类型重新命名;
一般格式为;typedef 已有类型 新类型名;
typedef int Elem; 
typedef struct{
    int date;
    .....
    .....
}STUDENT;
STUDENT stu1,stu2;

声明与定义:

struct Student
{
	char 	name[20];
	int 		age;
	int		score[5];
}struct Student s1 = {"zhangsan",22,{1,2,3,4,10}};
1.一个有意思的问题
  1. 在结构体内
//定义链表节点结构体
typedef struct ListNode{
	int val;
	struct ListNode* next;
}Node,*N;
//创建结构体指针
struct ListNode* p1;  
Node* p2;
N p3;      //此时的N相当于struct ListNode*      
//以上三种方式均创建了结构体指针
  1. typedef 对数据类型的妙用
typedef int *a, b;
int c = 5;
a d = &c;  //此时a相当于int* 
b e = 6;   //b相当于int
  1. 总结

typedef中别名中带“”,那么这个别名就相当于原名加“

2. 结构体嵌套(自引用)

参考:CSDN

  1. 自引用结构体
    1.1 不使用typedef时

错误的方式:

struct tag_1{
    struct tag_1 A;  
    int value;
};
    这种声明是错误的,因为这种声明实际上是一个无限循环,成员b是一个结构体,b的内部还会有成员是结构体,依次下去,无线循环。在分配内存的时候,由于无限嵌套,也无法确定这个结构体的长度,所以这种方式是非法的。

正确的方式: (使用指针)

struct tag_1{
    struct tag_1 *A; 
    int value;
};
    由于指针的长度是确定的(在32位机器上指针长度为4),所以编译器能够确定该结构体的长度。

1.2 使用typedef 时

错误的方式:

typedef struct {
    int value;
    NODE *link; 
} NODE;

正确的方式:

typedef struct{
int val;
struct NODE* link:
}NODE;

这里的目的是使用typedef为结构体创建一个别名NODE。但是这里是错误的,因为类型名的作用域是从语句的结尾开始,而在结构体内部是不能使用的,因为还没定义。

另外的正确的方式:有三种,差别不大,使用哪种都可以。

typedef struct tag_1{
    int value;
    struct tag_1 *link; 
} NODE;



struct tag_2;
typedef struct tag_2 NODE;
struct tag_2{
    int value;
    NODE *link;   
};



struct tag_3{
    int value;
    struct tag_3 *link; 
};
typedef struct tag_3 NODE;

5、一些重点名词

0、 指针运算

指针和整数或则指针的加减运算!
1. 指针加减整数

 #include <stdio.h>
int main(void) { 
    int urn[5] = {100,200,300,400,500};
    int *ptr1,*ptr2,*ptr3;
    
    ptr1 = urn;	//将数组首地址赋给指针
    ptr2 = &urn[2];	//将数组中第三个元素的地址赋给指针
    
    //指针加法
    ptr3 = ptr1 + 4;
    printf("ptr1 = %p , ptr3 = %p , *ptr3 = %d", ptr1,ptr3,*ptr3);


	return 0;
}

运行结果:

ptr1 = 0x7ffc62e2bec0
ptr3 = 0x7ffc62e2bed0 ==> ptr3 = ptr1+4 = ptr1地址+4*sizeof(int)
*ptr3 = 500
//整数是第2个运算对象。该整数将乘以指针指向类型的大小(以字节为单位)

2. 指针减指针

#include <stdio.h>

int main(void) { 
    int urn[5] = {100,200,300,400,500};
    int *ptr1,*ptr2,n;
    
    ptr1 = &urn[2];	
    ptr2 = &urn[4];	
    
    //指针减法
    n = ptr2 - ptr1;
    printf("ptr2 = %p , ptr1 = %p , n = %d", ptr2,ptr1,n);

	return 0;
}

运行结果:

ptr2 = 0x7fff9820f020
ptr1 = 0x7fff9820f018 //16进制下两者差8字节(两个元素的差距)
n = 2

1、 指针函数/函数指针

  • 1.指针函数(指针的函数)
    先看下面的函数声明,注意,此函数有返回值,返回值为int *,即返回值是指针类型的。
int *f(int a, int b);
  • 2.函数指针(函数的指针)

顾名思义,函数指针说的就是一个指针,但这个指针指向的函数,不是普通的基本数据类型或者类对象。

函数指针的定义如下:

// 1、声明函数指针
int (*f)(int a, int b); 


// 2、函数指针的使用
int max(int a, int b)
{
    return a > b ? a : b;
}
int*p)(int , int); //函数指针的定义
p = max;              //函数指针初始化
//调用
int c=p(a,b);         //函数指针的调用

推荐写法:点击链接查看和 Kimi 智能助手的对话 https://kimi.moonshot.cn/share/cqe885g17fm4bh015ih0

2、 指针数组/数组指针/指针常量数组

指针数组/数组指针 参考CSDN

  • 指针数组

存储指针的数组,是个数组,数组中的所有元素都是指针;

int* a[10];   //a是int* [6]型,a++就增加了整个数组的空间

在这里插入图片描述

int main()
{
	int a = 1;
	int b = 2;
	int *p[2];
	p[0] = &a;
	p[1] = &b;
	
//将二维数组赋给指针数组
	int *pp[3]; //一个一维数组内存放着三个指针变量,分别是p[0]、p[1]、p[2],所以要分别赋值
	int c[3][4];
	for (int i = 0; i<3; i++)
		pp[i] = c[i];
 return 0;
}
  • 数组指针

指向数组的指针,指向一个一维数组的指针;

int (*a) [10];

在这里插入图片描述

int main()
{
	//一维数组
	int a[5] = { 1, 2, 3, 4, 5 };
	//步长为5的数组指针,即数组里有5个元素
	int (*p)[5];
	//把数组a的地址赋给p,则p为数组a的地址,则*p表示数组a本身
	p = &a;

//将二维数组赋给指针
	int b[3][4];
	int(*pp)[4]; //定义一个数组指针,指向含4个元素的一维数组
	pp = b; //将该二维数组的首地址赋给pp,也就是b[0]或&b[0],二维数组中pp=b和pp=&b[0]是等价的
	pp++; //pp=pp+1,该语句执行过后pp的指向从行b[0][]变为了行b[1][],pp=&b[1]
	return 0;
}
  • 指针常量数组
char* const argv[];
//表明数组里的元素都是指针,并且指针指向的地址不变(指针常量)
  • 常量数组指针
char const (*argv)[];
char const (*argv)[]:将上述部分组合起来,这表示 argv 是一个指针,它指向一个数组,该数组的元素是指向常量字符的指针。

2.0 关于一维数组 / 二维数组的步长问题

#include <stdio.h>

int main()
{
	//一维数组
	int a[5] = {1,2,3,4};
	printf("a    的地址%p\n",a);
	printf("a+1的地址%p\n",a + 1);  //地址增加了4字节(一个元素的长度)
	//一维数组的指针
	int *p = a;
	printf("p    的地址%p\n",p);    //p+1的步长等价于a+1
	printf("p+1的地址%p\n",p + 1);
	//一维数组取地址
	printf("&a    的地址%p\n",&a);    //&a相当于是一个步长为总体数组空间的数组指针(*P)[5]
	printf("&a+1的地址%p\n",&a + 1);  //地址增加了20字节(整个数组的长度)
	//数组指针
	int (*q)[5] = &a;
	printf("q    的地址%p\n",q);      //q+1的步长等价于&a+1
	printf("q+1的地址%p\n",q + 1);
	//二维数组
	int c[3][4] = {1,2,3,4};
	int (*b)[4] = c;       //数组指针(二维数组),每次步长为一行元素
	printf("b    的地址%p\n",b);
	printf("b+1的地址%p\n",b + 1);  //地址增加了16字节(一行元素的长度)	
   return 0;
}

在这里插入图片描述

  • 解释:
    数组名a只表示数组的首地址,只有(&a)时表示整个数组,此时其步长为整个数组也就是(sizeof(a));

3、 指针常量/常量指针

Difference between:

  • const char *p, //常量的指针
  • char * const p //指针(指向的地址)是常量
  • const char * const p
    加深记忆记住三句话:

指针“*”和 const 谁在前先读谁 ;
*象征着地址,const象征着内容;
谁在前面谁就不允许改变。

例如:
int const *p1 = &b; //const 在前,定义为常量指针
int *const p2 = &c; // *在前,定义为指针常量


int const * a; 与const int * a;
是没有任何区别的,它们的效果是一样的,
  • 常量指针是指指向常量的指针,顾名思义,就是指针指向的是常量,即,它不能指向变量,它指向的内容不能被改变,不能通过指针来修改它指向的内容,但是指针自身不是常量,它自身的值可以改变,从而指向另一个常量。指针指向的内容不可修改,但是指针指向的地址可以改变。

  • 指针常量是指指针本身是常量。它指向的地址是不可改变的,但地址里的内容可以通过指针改变。它指向的地址将伴其一生,直到生命周期结束。有一点需要注意的是,指针常量在定义时必须同时赋初值指针指向的地址不可改变,但是指针指向的内容可以改变。

  • 常量指针常量 const char * const p 指针指向的地址和内容都不可修改。

4、数字和字符串相互转换(atoi…)

参考CSDN

  • 将字符串转化为int型
  1. 头文件:#include <stdlib.h>

  2. 函数声明

int atoi(const char *nptr);
long atol(const char *nptr);
long long atoll(const char *nptr);
long long atoq(const char *nptr);

  1. 功能
    atoi:把字符串nptr转换为int。

atol:把字符串nptr转换为long int。

atol:把字符串nptr转换为long long int。

atoq:atoq() is an obsolete name for atoll()。

  1. 示例
/*
 * 程序名:book.c,此程序用于演示atoi函数族。
 * 作者:C语言技术网(www.freecplus.net) 日期:20190525。
*/
int main()
{
  int ii=0;
  ii=atoi("123");
  printf("ii=%d\n",ii);  // 输出ii=123

  ii=atoi("123abc");
  printf("ii=%d\n",ii);  // 输出ii=123,合法数字后的abc被忽略。

  ii=atoi("abc123");
  printf("ii=%d\n",ii);  // 输出ii=0,数字前有字符为非法。

  ii=atoi("+123");
  printf("ii=%d\n",ii);  // 输出ii=123,+是合法字符。

  ii=atoi("-123");
  printf("ii=%d\n",ii);  // 输出ii=-123,-是合法字符。
}

atoi()函数

char str[]="123465";
int i=atoi(str);
printf("%d",i);
  • 数字转字符串
//itoa():将整型值转换为字符串。
# include <stdio.h>
# include <stdlib.h>
void main (void)
{
int num = 100;
char str[25];
itoa(num, str, 10);
printf("The number 'num' is %d and the string 'str' is %s. \n" ,
num, str);
}

itoa()函数有3个参数:第一个参数是要转换的数字,
第二个参数是要写入转换结果的目标字符串,
第三个参数是转移数字时所用 的基数。
在上例中,转换基数为1010:十进制;2:二进制...
  • 宏定义将数字转字符串
#define STR(x)  #x
//调用
STR(100);

5、结构体数组

定义:该数组的每个元素都是一个结构体。
在这里插入图片描述
结构体数组的初始化及直接输出方法:

/*
*copyright(c) 2018,HH
*All rights reserved.
*作 者:HH
*完成日期:2018年8月16日
*版本号:v1.0
*
*问题描述:结构体数组的正确初始化及输出
*输入描述:;
*程序输出:
*/
 
#include<stdio.h>
#include<string.h>
struct student
{
    int num;
    char name[20];
    char sex;
    int age;
    float score;
    char addr[30];
};
int main()
{
    int i;
    struct student stu[3]={{10101,"LiuHu",'F',18,99.7,"beijing road"},{10102,"NaoDan",'M',17,99.8,"shanghai road"},{10103,"MaHuangTeng",'F',48,99.9,"shenzhen road"}};
    for(i=0;i<3;i++)
    {
       printf("%d %s %c %d %1f %s\n",stu[i].num,stu[i].name,stu[i].sex,stu[i].age,stu[i].score,stu[i].addr);
    }
    return 0;
}

在这里插入图片描述

5.1结构体递归嵌套

结构体的自引用(self reference),就是在结构体内部,包含指向自身类型结构体的指针

结构体的相互引用(mutual reference),就是说在多个结构体中,都包含指向其他结构体的指针。

  1. 自引用结构体
    1.1 不使用typedef时

错误的方式:

struct tag_1{
    struct tag_1 A;  
    int value;
};

这种声明是错误的,因为这种声明实际上是一个无限循环,成员b是一个结构体,b的内部还会有成员是结构体,依次下去,无线循环。在分配内存的时候,由于无限嵌套,也无法确定这个结构体的长度,所以这种方式是非法的。

正确的方式: (使用指针)

struct tag_1{
    struct tag_1 *A; 
    int value;
};

由于指针的长度是确定的(在32位机器上指针长度为4),所以编译器能够确定该结构体的长度。

1.2 使用typedef 时

错误的方式:

typedef struct {
    int value;
    NODE *link; 
} NODE;

这里的目的是使用typedef为结构体创建一个别名NODEP。但是这里是错误的,因为类型名的作用域是从语句的结尾开始,而在结构体内部是不能使用的,因为还没定义。

正确的方式:有三种,差别不大,使用哪种都可以。

typedef struct tag_1{
    int value;
    struct tag_1 *link; 
} NODE;



struct tag_2;
typedef struct tag_2 NODE;
struct tag_2{
    int value;
    NODE *link;   
};



struct tag_3{
    int value;
    struct tag_3 *link; 
};
typedef struct tag_3 NODE;

2、C库函数/操作符----------------------

函数的奇怪知识——————

1.函数内嵌 / 嵌套函数

  1. 函数内嵌类似函数内联,函数嵌套是函数实现在函数体内部,只有父函数能够调用
    嵌套函数
    不能在外部声明
  2. 可以将嵌套函数的地址传递给其他函数,并由其他函数调用,就好像可以传递其他局部变量的地址
  3. 嵌套函数和父函数可访问同样地变量,但只能访问比嵌套函数声明早的局部变量
  4. 嵌套函数可以使用goto语句跳转到函数之外的某个标号位置,该位置应该位于父函数内部
  5. 通过将其声明为auto,即可声明嵌套函数的原型
void right() {
	auto double hypotenuse();
	double a = 3.0;
	double b = 4.0;
	double hypotenuse(double x, double y) {
		return (sqrt(x * x + y * y));
	}
	printf("Long side of %lf and %lf is %lf\n", a, b, hypotenuse(a, b));
}

常用函数—————————

1. for

for(i=0;i<a;i++)
for(i=0;i<a;++i)  //因为都是先执语句再执行i自加,因此两者并无区别

for详解:
for(表达式1; 表达式2; 表达式3) 语句
它的执行过程如下:

  1. 先求解表达式1。
  2. 求解表达式2,若其值为真(非0),则执行for语句中指定的内嵌语句,然后执行下面第3步;若其值为假(0),则结束循环,转到第5)步。
  3. 求解表达式3。
  4. (转回上面第2)步继续执行。
  5. 循环结束,执行for语句下面的一个语句。

1.1switch()

switch中多个case值对应一个执行语句

#include <stdio.h>
int main()
{
        int a=0;
        printf("input value a:\n");
        scanf("%d",&a);
        
        switch(a)
        {
                case 1:
                case 2:
                case 3:
                        {
                                printf("hello one!\n");
                                break;
                        }
                case 4:
                case 5:
                case 6:
                        {
                                printf("hello two!\n");
                                break;
                        }
        }

        return 0;
}

google@ubuntu1404:~/workspace/DLT/645$ ./a.out 
input value a:
1
hello one!
google@ubuntu1404:~/workspace/DLT/645$ ./a.out 
input value a:
2
hello one!
google@ubuntu1404:~/workspace/DLT/645$ ./a.out 
input value a:
4
hello two!
google@ubuntu1404:~/workspace/DLT/645$ ./a.out 
input value a:
5
hello two!
google@ubuntu1404:~/workspace/DLT/645$ 

上面例子中
case值为1\2\3时都是执行打印 hello one!的操作

case值为4\5\6时都是执行打印hello two!的操作

2.scanf

scanf详解:
scanf()函数接收输入数据时,遇以下情况结束一个数据的输入:
① 遇空格、“回车”、“跳格”键。
② 遇宽度结束。
③ 遇非法输入。

  1. 利用scanf输入字符串:
    char a[10] = {0};
    scanf("%s",a);
  1. scanf输入不定长数组:
    法1:
    int a[10] = {0};
    int i = 0;
    while(scanf("%d",&a[i]) != EOF){
        printf("%d\n",a[i]);
        i++;
    }

法2:

    int a[100]={0};
    int length=0;
do{
    scanf("%d",&a[length++]);
}while(getchar()!='\n');         //scanf输入不定长数组

在这里插入图片描述

  1. 利用scanf输入两行数字
    利用scanf输入两行数字:(第一行为数组元素个数,第二行为数组)
    利用malloc动态分配内存空间:
int length;
int i = 0;
int* a = NULL;
scanf("%d",&length);
a = (int*)malloc(sizeof(int)*length);
do{
scanf("%d", &a[i++]);
}wheil(getchar()!='\n');

2.1、getchar()/putchar()

  • getchar()

用于读取用户从键盘输入的单个字符,它有一个整型的返回值,当发生读取错误的时候,返回整型值-1,当读取正确的时候,它会返回用户从键盘输的第一个字符的ASCII码, 当程 序调用getchar时.运行程序时 就等着用户从按 键输入, 用户输入的字符被 存 放在键盘缓冲区中.直到用户按回车为 止(回 车字符也放在缓冲区 中),当用户键入回车之后,getchar才开始从输入流中每次读入一个字符,输入的字符不只一个的时候,后续的getchar调用不会等待用户按键,而直接读取缓冲区中的字符,直到缓冲区中的字符读完之后,才等待用户按键,getchar函数输入数字也按字符处理,单个的getchar函数输入多于一个字符时,只接收第一个字符

  • putchar()

向终端输出一个字符。其格式为putchar(ch),其中ch可以是被单引号(英文状态下)引起来的一个字符,可以是介于0~127之间的一个十进制整型数(包含0和127)(超过127就不是ASCII码了),也可以是事先用char定义好的一个字符型变量 当c为一个被单引号(英文状态下)引起来的字符时,输出该字符(注:该字符也可为转义字符 ), 当c为一个介于0~127(包括0及127)之间的十进制整型数时,它会被视为对应字符的ASCII代码,输出该ASCII代码对应的字符; 当c为一个事先用char定义好的字符型变量时,输出该变量所指向的字符。 当整型变量ch超出8位变量的范围时,ch则会变强制转化为8位变量(即取其低八位传过去输出),当为负数的时候,由于计算机存储负数是用补码表示的,所以传过去的二进制补码也被当做正数处理,也是取其低八位
getchar()和putchar()函数包含在头文件stdio.h中,使用时需包含

2.1、gets()/puts()

**

  • gets()

的作用是从键盘上读取字符串,跟scanf() 相似,但又有所不同。
gets() 函数的用法:
gets(字符串的首地址);
例:

char str [20]; 
gets(str);

须要注意的是:
(1) gets() 可以接收空格、Tab键, 碰到回车键输入才结束;而scanf碰到空格、回车、Tab键 都会结束输入。
(2) gets() 和scanf() 都不能检测越界。

#include<stdio.h>

int main(void) {
    //char str[20];
    char* str = (char*)malloc(sizeof(char)*20);
    gets(str);    //键盘输入all is well
    //char str[] = "all is well well";
    puts(str);
    return 0;
}

在这里插入图片描述

  • puts()

的作用是 向显示屏输出字符串并换行。
用法:
puts (字符串的首地址) ;
例:

char str[]=" helloworld";
puts(str);  //printf需要添加"\n'用来换行,而puts在 输出的时候会将字符串末尾的\0'自动换成\n'。

3.printf

1、%4.1f 4.1分别代表什么

printf("%4.1f",a);

%f是直输出实型数据,“4.1"是对输出的数据做一个规定,夹在”%“和"f"中间,'4.1"中的"4"是指输出总共四位(即四个字符),包括数字,小数点,和空格,他们各占一个字符而”."后面的这个数字"1"是指小数点后保留一位.
例如:

float b = 3.42;
printf("%4.1f\n", b);

输出即为:
_3.4
_代表空格,输出的数据占4位,如果还有一个数据是34.2,也只用4.1f输出,这样两条数据就会对齐输出。
正整数(4)代表右对齐,不足左边补空格,负整数(-4)代表左对齐,不足右边补空格。
2、

%d左对齐,输出变量的所有数字;
%4d右对齐,宽度为4,左边填充空格,
当变量的实际宽度大于4时,输出变量的所有数字;
%04d与%4d的唯一区别就是左边填充0%-4d意思是数字占四位、左对齐。
```c
比如,
1.%d,%4d,%04d,%-4d输出12时,结果是:
12
两个空格12
0012
12_ _    //个人推测

3.1.sprintf() / snprintf()

  • sprintf函数的格式:
int sprintf( char *buffer, const char *format [, argument,...] );

除了前两个参数固定外,可选参数可以是任意个。buffer是字符数组名;format是格式化字符串

常用方式:

sprintf函数的功能与printf函数的功能基本一样,只是它把结果输出到指定的字符串中了,看个例子就明白了:
例:将”test 1 2”写入数组s中

char s[40]; sprintf(s,"%s%d%c","test",1,'2');

例如,在处理传感器数据时,为了将得到的数据整合成特定的格式通过网络发送出去,

char buffer[100] = { 0 };
sprintf(buffer, "temperature: %f; humidity:%f\r\n", tempData, humiData);
send(clientSocket, buffer, strlen(buffer));
  • snprintf() 详解
    函数原型:
int snprintf(char *restrict buf, size_t n, const char * restrict  format, ...);

函数说明:最多从源串中拷贝 n-1 个字符到目标串中,然后再在后面加一个 ‘\0’。所以如果目标串的大小为 n 的话,将不会溢出。

函数返回值:若成功则返回欲写入的字符串长度,若出错则返回负值。
1、推荐的用法

#include <stdio.h>

 

int main(int argc, char **argv)

{

    char str[10] = {0,};

    snprintf(str, sizeof(str), "0123456789012345678");

    printf("str=%s\n", str);

 

    return 0;

}

输出:str=012345678

3.2.fscanf()/fprintf()

参考CSDN
**1.fscanf()😗*从文件中读取
fscanf()函数的头文件是<stdio.h>,

函数原型 为 int fscanf(FILEstream, constcharformat, [argument…]);

其功能为根据数据格式(format)从输入流(stream)中写入数据(argument);

【参数】stream为文件指针,format为格式化字符串,argument 为格式化控制符对应的参数。

从文件指针fp指向的文件中,按format中对应的控制格式读取数据,并存储在agars对应的变量中

原型: fscanf(FILE *fp, const char *format, agars)

#include<stdio.h>
#include<stdlib.h>
 
int main()
 
{
    FILE *fp;
    char ch;
    fp = fopen("test.txt","r");
 
    if(fp == NULL)
    {
         printf("Open filefailure!");
         exit(1);
    }
 
    else
    {
         fscanf(fp,"%s",&ch);
    }
 
    printf("%s\n",ch);   
    fclose(fp);
    return 0;
}

注:对于上面else中的内容需注意一下几点:

1.如果要读取一个整数(该整数必须在所存变量的数据类型表示的范围之内)则为:fscanf(fp, “%d”, &ch),而此时ch应该定义为int;若读取的数据大于int所能表示的范围,则读取的数据屏幕显示为负数,即读取的数据发生越界,如果此时的ch依然为char型,则运行时报错(内存读写错误)。

2.如果要读取字符串,则ch应该定义为char型数组或指针(指针需分配空间),而不能将其定义为char型,否则也会报错(内存读写错误);

3.输出数据时的数据格式应该和读取数据时的控制格式相同,除非进行强制转换。

4.使用fscanf()时,其中的变量agars应该取其地址;

5.对于文件的操作,记得文件打开操作后要关闭。

对于fscanf()主要应用:

按行读取一个文件中的所有内容 或 依次读取每行相隔的几个数据,具体参照以下示例:

#include<stdio.h>
#include<stdlib.h>
 
int main()
{
    FILE *fp;
    char *ch, *ah;
    ch =(char *) malloc(sizeof(char) * 100);
    ah =(char *) malloc(sizeof(char) * 100);
    fp = fopen("test.txt","r");
 
    if(fp == NULL)
    {
         printf("Open filefailure!");
         exit(1);
    }
 
    else
    {
         while(!feof(fp))
        {
             fscanf(fp,%s”, ch);
             printf(%s”, ch);//这两行为按行读取所有数据
             fscanf(fp,%s%s”, ch, ah);
             printf(“The value of ch and ah is:%s %s\n”,ch,ah);//这两行为分别读取每行相隔的几个数据            
        }
    }
 
    printf("%s\n",ch);
 
    free(ch);
    free(ah);
 
    fclose(fp);
 
    return 0;
} 

2. fprintf() 向文件中写入
**😗*喜提一个表情

将agars(参数表)内各项的值,按format(格式控制字符串)所表示的格式,将数据格式为字符串的形式写入到文件指针fp指向的文件中

原型:fprintf(FILE *fp, const char *format, agars)

fprintf()和fscanf()相对应,其用法也基本和fscanf()相同。具体参照以下示例:

#include<stdio.h>
#include<stdlib.h>
 
int main()
{
    FILE *fp;
    fp = fopen("test.txt","a+");
 
    fprintf(fp,%d %d”,123456,789);//将123456和789写到test.txt文件中
 
    fprintf(fp,"%s %s","China","ChongQing"); //将字符串China和ChongQing追加写到test.txt文件中
 
    fclose(fp);
 
    return 0;
 
}

3.3

4.sizeof()/strlen()

  • strlen()求某个字符串的长度时是不包括结尾标志符’\0’的
  • sizeof()求某个字符串占用的内存空间时,结尾字符’\0’是被包括在里面的。
 char str[] = "Hello";
 printf("%d\n",sizeof(str));   //输出6
 printf("%d\n",strlen(str));   //输出5

4.1strcat函数

一、认识strcat
strcat函数又被称为是字符串追加/连接函数,它的功能就是在一个字符串后面追加上另外一个字符串。
使用方式如下:

#include<string.h>
#include<stdio.h>
int main()
{
	char arr1[20] = "hello ";
	char arr2[] = "world";
	printf("%s\n", strcat(arr1, arr2));
	return 0;
}
输出: hellow world

库函数中strcat函数的声明是char * strcat ( char * destination, const char * source );
经过测试,我们发现strcat的实现模式是将src中的所有字符(连同字符串最后的’\0’一起)加到dest字符串中第一个‘\0’的位置,具体一点说就是将dest中第一个’\0’替换成src的第一个字符,然后该’\0’后的字符替换成src的第二个字符,后面以此类推。

4.2 strcpy / strncpy

参考CSDN

头文件:

#include <string.h>
  1. strcpy

strcpy()函数是C语言中的一个复制字符串的库函数
**功能:**就是将前面的字符内容串换成后面的字符串

  • 函数原型
char *strcpy(char *dst, const char *src);
  • 函数重写
char * strcpy(char *dst,const char *src)   
{
    if((dst==NULL)||(src==NULL)) return NULL; //[0]空指针检查

    char *p = dst; //用p代替dst移动,后面返回des就很完美;
 
    while ((*p++=*src++)!='\0'); //[2]strcpy函数遇到'\0'将会停止
 
    return dst;//[3]
}
  • 例子
    在这里插入图片描述
    之前在某处看到过这个问题,以下将对其进行详解,

· 最终答案输出是 “ABC\0”; '\0’是不打印出来的

· 为什么答案不是"ABC\0ef";

[1]从strcpy函数的实现代码可以看出当src指针指向为‘\0’时将会停止字符串的复制,由此可以得知返回ret指针所指向的数组a内容应该是 “ABC\0ef”,也就是说实际内存数组a中的内容应该是“ABC\0ef“;但是为什么最终显示会是"ABC\0"呢,原因在于,strcpy的本身属性:即strcpy只用于字符串复制,并且它不仅复制字符串内容之外,还会复制字符串的结束符;

基于此种原因,但是字符串的特性是什么呢?字符串最后一个字节存放的是一个空字符——“\0”,用来表示字符串的结束。把b复制到a之后会令b的空字符把复制后的字符串隔断,所以最终printf输出只能是"ABC\0";

char *a="coda";
      char b[MAX]="you are the best one.";
      char *p;
      p=strcpy(b+8,a);
      puts(p);

输出结果为:coda

  1. strncpy

strncpy函数是C语言中的内置函数之一,相较于strcpy函数,他使用更加灵活,功能更加强大。
**功能:**就是用后面的字符串覆盖前面字符串的对应位置

  • 函数原型:
 char * strncpy ( char * destination, const char * source, size_t num );

strncpy共有三个参数,分别是字符串目的地,字符串源头,以及选择copy的字符数目
strncpy情况与strcpy基本保持一致,但源头数据长度与copy字符的数目会产生一些问题:

  • 函数重写
char *myStrncpy(char *dest, const char *src, int n){
    char *d = dest;
    char *s = src;
    if(!dest || !src ) return NULL;
    while(n--) *d++ = *s++;
    return dest;
}

当字符串源头的长度大于等于copy数目时,根据字符数目照常复制即可;

当字符串源头的长度小于copy的数目时,剩下几个copy数目,就在字符串目的地后加几个‘\0’。

下图为两种情况的演示:
在这里插入图片描述

5.malloc/realloc/calloc函数的区别

参考CSDN

三者都是分配内存,都是stdlib.h库里的函数,有些编译器需要#include <malloc.h>
1、malloc申请后空间的值是随机的,并没有进行初始化,而calloc却在申请后,对空间逐一进行初始化,并设置值为0;

  1. malloc申请一个动态数组(也可参考scanf()小节)
int *p = (int *)malloc(20*sizeof(int))//malloc
  • 注意:上面只是分配了一段连续内存并让指针p指向了这块内存的起始地址,但**sizeof(p)**依然等于8;
  • 如果采用 int p[ ] = {1,2,3},那么sizeof(p)才会不仅仅是指针所占得空间。
int main(){
int *p = NULL;
int n = 10;
p = (int*)malloc(sizeof(int)*n);
//mallo函数返回的实际是一个无类型指针,必须在其前面加上指针类型强制转换才可以使用
//如果分配成功则返回指向被分配内存的指针,否则返回空指针NULL。
 *p = 100;
//使用完P后如果不进行内存释放会导致内存泄漏
//指针使用完成之后free(p),只是将申请的内存释放掉,但是释放掉的内存会被填充成垃圾
//但指针还是指向这快内存,只有程序结束后指针才会被销毁,
//因此为了防止无意间对指针解引用,最后还要将指针指向NULL;
free(p);
p = NULL;
return 0;
}
  1. calloc
int *p = (int *)calloc(20, sizeof(int); //calloc不需人为计算空间大小
  1. realloc:对动态内存进行扩容

功能:先判断当前的指针是否有足够的连续空间,
1、如果有,扩大mem_address指向的地址,并且将mem_address返回,2、如果空间不够,先按照newsize指定的大小分配空间,将原有数据从头到尾拷贝到新分配的内存区域,而后释放原来mem_address所指内存区域
3、(注意:原来指针是自动释放,不需要使用free),同时返回新分配的内存区域的首地址。即重新分配存储器块的地址。

返回值:如果重新分配成功则返回指向被分配内存的指针,否则返回空指针NULL。

注意:这里原始内存中的数据还是保持不变的。当内存不再使用时,应使用free()函数将内存块释放。

  • 代码验证:
#include<stdio.h>
#include<stdlib.h>
#define N 10000
int main()
{
	int i;
	int *pn=(int *)malloc(5*sizeof(int));
	printf("%p\n",pn);
	for(i=0;i<5;i++)
		pn[i] = i+1;
 
	printf("%d\n",sizeof(int));
	int *pnn=(int *)realloc(pn, N*sizeof(int));
	printf("%p\n",pnn);
	for(i=0;i<5;i++)
	printf("%3d",pnn[i]);
	
	//for(i=0;i<5;i++)
	//printf("%3d",pn[i]);
	printf("\n");
	//free(pn);
	free(pnn);       //这里为什么不将pnn指向NULL
	return 0;
}

//N取值较小时,输出如下
0x12ee010
4
0x12ee010
 1  2  3  4  5
 
//N取值较大时,例如100000 输出如下
0xef8010
4
0x7fba7062e010
 1  2  3  4  5
  • 代码实验+1:
realloc
1、该函数将 ptr 指向的内存大小修改为size。内存中的数据从开头到size保留不变。
2、如果size大于原内存大小,则多出的部分会被初始化。
3、如果ptr是NULL,此时 realloc 就相当于 malloc 函数。
4、如果ptr不为NULL,size=0,那么 realloc 就相当于 free函数。
5、另外有一点需要注意,如果 ptr 不为 NULL,那么 ptr 必须是之前 malloc,calloc,realloc 函数返回的指针,不能任意指定。
int main(void)
{
	const int size = 2000;
	int *p = (int *)malloc(20*sizeof(int));
	int *pp = (int *)realloc(p, size*sizeof(int));
	
	printf("原来的p_Address:%x   扩容后的pp_Address:%x \n\n", p, pp);
	
	return 0;
}

在这里插入图片描述

1\如果size较大,原来申请的空间后面没有足够大的空间扩容,系统将重新申请一块(20+size)*sizeof(int)的内存,并把原来空间的内容拷贝过去,原来空间free;
注意:如果扩容后的内存空间较原空间小,将会出现数据丢失,如果直接realloc(p, 0);相当于free(p)

6、struct/enum/union

  • 三者的空间占用情况

struct:所有元素所占空间相加,并且要和最大数据类型字节对齐(最终值要增大至最大数据类型的整数倍);
enum:最大数据类型的空间,并且要和最大数据类型字节对齐;
union:最大数据类型所占空间,并且要和最大数据类型字节对齐;

2、enum

在 C 和 C++ 的枚举类型中,如果枚举元素没有显式地被赋值,则它们的值将会默认递增。而第一个元素的默认值是 0。后续的元素的值将会在前一个元素的基础上递增。

声明枚举类型并赋值:enum Month a = Feb; // enum Month是声明枚举时的开头

一、一次定义多个常量。
比如说我们的程序中处理问题时与星期几有关,可能要将星期一转换为数字1,星期二转换为数字2,一直到数字7,在不用enum关键字的情况下,可以使用define来定义,但是大家会觉得很麻烦,因为你要一个一个的定义,星期的还好说,只有7天,如果是月份呢,一年有12个月份,那就要写12个define,非常的不方面,如果利用enum的话就会非常的方便。

#include<stdio.h>
 
enum week {Mon=1,Tue,Wed,Thu,Fri,Sat,Sun};
 
int main()
{
    printf("%d",Tue);
    return 0;
}

这样定义以后,Mon的值为1,Tue的值为2,Wed的值为3,一次类推。
然后就可以像使用define之后的常量一样的使用定义的7个值了。
二、限定变量的范围

比如我们的应用程序中要处理有关月份的东西,显然月份只能取1-12中的某个数字,为了保证程序的正确性和健壮性,我们应该使用enum。

#include<stdio.h>
 
enum Month {Jan=1,Feb,Mar,Apr,May,Jun,Jul,Aug,Sep,Oct,Nov,dec};
 
int main()
{
    enum Month a =  Feb;
    printf("%d",a);
    return 0;
}

比如像这样,定义的枚举类型 a 的取值只能是那12个变量中的一个,
如果赋予了其他的变量,编译器就会报错。

注意与宏定义区别:

enum用来定义一系列宏定义常量区别用,相当于一系列的#define xx xx,当然它后面的标识符也可当作一个类型标识符;typedef enum则是用来定义一个数据类型,那么该类型的变量值只能在enum定义的范围内取。两者在这点上是没有差别的。

typedef enum {}
在 C 和 C++ 编程中,typedef enum {} 是用来定义匿名的枚举类型。

typedef enum {
    MONDAY,
    TUESDAY,
    WEDNESDAY,
    THURSDAY,
    FRIDAY,
    SATURDAY,
    SUNDAY
} Weekday;

Weekday 是一个枚举类型的别名,它包含了七个常量值,分别表示一周的每一天。
而在 typedef enum {} 中的花括号内部,你可以定义枚举类型时不指定名称,从而创建一个匿名的枚举类型。这在某些情况下可以用来简化代码或者限制作用域。

3、union

//example
所以说,管union的叫共用体还真是贴切——完全就是共用一个内存首地址,
并且各种变量名都可以同时使用,操作也是共同生效。

#include<stdio.h>
union var{
        long int l;
        int i;
};
main(){
        union var v;
        v.l = 5;
        printf("v.l is %d\n",v.i);
        v.i = 6;
        printf("now v.l is %d! the address is %p\n",v.l,&v.l);
        printf("now v.i is %d! the address is %p\n",v.i,&v.i);
}
结果:
v.l is 5
now v.l is 6! the address is 0xbfad1e2c
now v.i is 6! the address is 0xbfad1e2c
 

7、goto

  1. 一般用法
    要跳转的语句一般写在main()函数内的最下面
#include<stdio.h>

int main() {
	int c = 1;
	if (c) {
		goto start;
	}

start:
	printf("实例1\n");
	printf("实例2\n");
	printf("实例3\n");
	printf("实例4\n");
	printf("实例5\n");
}

  1. goto实现循环
/*用goto实现1到100求和*/
int main(void){
	int i = 1, sum = 0;
	loop:
		if(i<=100){
		sum += i;
		i++;
		goto loop;
	}
	printf("sum = %d\n",sum);
	return 0;
}
  1. 多重嵌套语句用goto跳出
    例:凑硬币
 #include <stdio.h>
//接力break
int main()
{

int x;
int one, two, five;

printf("请输入要凑的钱数,单位(元)\n");
scanf("%d",&x);
for ( one=1;one < x*10; one ++){
	for ( two=1;two < x*10/2; two ++){
		for ( five=1;five < x*10/5; five ++){
		
			if(one + two*2 + five*5 == x*10){
			
				printf("可以用%d个一角加%d个两角再加%d个五角得到%d元\n\n",one,two,five,x);

				goto out;
			}

		}

	}

}

out:
return 0;	
}

(这样情景使用goto,其他场景不建议使用goto)
从多层循环嵌套中退出时,只能使用goto语句? ,不一定(return 也可以结束多层循环)

8、可变参函数

C可变参函数使用

9、memset

memset(data, (size_t)0xFF, sizeof(data));

用size_t的好处:

  1. 类型转换:(size_t) 是一个类型转换操作符,它将括号内的值转换为 size_t 类型。size_t 是一个无符号整数类型,通常用于表示大小和长度。
  2. 这里,memset 的第二个参数需要是一个 int 类型的值,用于指定要填充的字节值。通过将 0xFF 转换为 size_t,你确保了即使在 int 类型比 size_t 类型小的平台上,赋值也能正确地以无符号形式处理。

memset是一个初始化函数,作用是将某一块内存中的全部设置为指定的值。

void *memset(void *s, int c, size_t n); 
  1. s指向要填充的内存块。
  2. c是要被设置的值。
  3. n是要被设置该值的字节数。
    返回类型是一个指向存储区s的指针。
//memset函数的作用就是在替换指定位置的值
//memset()函数有三个参数
//第一个参数为数组或者字符数组的名称(其实就是首地址)
//第二个参数为0或者-1或者单个字符(注意用单引号)
//具体使用看样例 
//第三个参数为改变的数组大小(或者称为长度) 
#include<stdio.h>
#include<string.h>

void Demo_1()//样例一 
{
	int a[10];//声明的是int数组(注意 ) 
	memset(a,-1,sizeof(int )*10);//第二个参数可以变为0
 	//此时的第二个参数不能为单个字符 (具体原因我不是很清楚)
 	//建议百度百科,似乎与字节有关 
 	//如果写成单个字符输出的结果为乱码 
 	//有兴趣可以试试 
	for(int i = 0; i<10 ; i++)
	{
		printf("%d ",a[i]);
	}
 } 
 void Demo_2()//样例二 
{
	char a[10];
	memset(a,'a',sizeof(char)*10);//第二个参数为单个字符 
	//此时的第二个参数不能为0或者-1 (具体原因我不是很清楚)
	//如果写成int值的话会变成空、
	//有兴趣可以试试 
	for(int i = 0; i<10 ; i++)
	{
		printf("%c ",a[i]);
	}
 }
 void Demo_3()//样例三 
{
	char a[] = "sdadasdaaqweqwsdas";//声明并赋值 
	memset(a,'a',sizeof(char)*5);//指定前5个元素 
	 
	for(int i = 0; i<10 ; i++)
	{
		printf("%c ",a[i]);
	}
 }
int main()
{
	printf("-------样例一---------\n");
	Demo_1(); 
	printf("\n-----样例二-----------\n");
	Demo_2();
	printf("\n-----样例三-----------\n");
	Demo_3();
 
}

在这里插入图片描述

  • 函数重写
void* myMemset(void* dest, int c, size_t count){
char* p = (char*)dest;   //memset是按照字节去替换的,因此强转成char;
if(!p) return NULL;
if(!count) return dest;
for(int i = 0; i < count; i++){
*p++ = c;
}
return dest;
}

10、memcpy

注意:确保源和目标内存区域不重叠,或者建议使用 memmove 代替 memcpy,因为 memmove 可以处理重叠的内存区域。
memcpy函数是C/C++语言中的一个用于内存复制的函数,声明在 string.h 中(C++是 cstring)。其原型是:

void *memcpy(void *destin, void *source, unsigned n);

作用是:以source指向的地址为起点,将连续的n个字节数据,复制到以destin指向的地址为起点的内存中。

函数有三个参数,

  1. 第一个是目标地址,第二个是源地址,第三个是数据长度。
    使用memcpy函数时,需要注意:
  2. 数据长度(第三个参数)的单位是字节(1byte = 8bit)。
  3. 注意该函数有一个返回值,类型是void*(万能指针),是一个指向destin的指针。
    memcpy函数复制的数据长度
    使用memcpy函数时,特别要注意数据长度。如果复制的数据类型是char,那么数据长度就等于元素的个数。而如果数据类型是其他(如int, double, 自定义结构体等),就要特别注意数据长度的值。
    好的习惯是,无论拷贝何种数据类型,都用 n * sizeof(type_name)的写法。
    char a[10] = "abcdefgh";
    unsigned n = 2;
    void * p = memcpy(a+3, a, n);

以上代码将从a开始的两个字节的数据(即’a’和’b’),复制到从a+3开始的内存('d’所在的地址)。这样,'d’和’e’被替换。
执行结束之后,字符数组(字符串)a的内容变为"abcabfgh",返回值p即为a的地址(p == a)。

  • 函数重写
    不考虑地址重叠时:
void *memcpy(void *dest, const void *src, size_t count)
{
	char *tmp = dest;                   //	char *p = (char*)dest;
	const char *s = src;                //	char *s = (char*)src;这样是不是规范一点
 
	while (count--)
		*tmp++ = *s++;
	return dest;
}


考虑地址重叠时:(src地址段和dest地址段有交叉,src地址段在前)

void* myMemcpy(void* des, const void* src, int n){
    if(des == NULL || src == NULL) return NULL;
    char* d = des;
    char* s = src;

    if(s < d && s + n > d){
        d += n;
        s += n;
        while(n--) *d-- = *s--;
    }else{
        while(n--) *d++ = *s++;  //*p++ 先引用p的值然后,实现*p的运算,然后使p自增1
    }
    return des;
}
  • 源代码实现
void * memcpy(void * memTo, const void * memFrom, size_t size)
{
  if((memTo == NULL) || (memFrom == NULL)) //memTo和memFrom必须有效
         return NULL;
  
  char * tempFrom = (char *)memFrom;             //保存memFrom首地址
  char * tempTo = (char *)memTo;                 //保存memTo首地址      
  
  while(size -- > 0)                //循环size次,复制memFrom的值到memTo中
  {
       *tempTo++ = *tempFrom++ ;  
       //*tempTo = *tempFrom ;
       //tempTo++;
       //tempFrom++;
    }
  return memTo;
}
//(1)可以拷贝任何类型的对象,因为函数的参数类型是void*(未定义类型指针),也就是说传进去的实参可以是int*,short*,char*等等,
//(2)由于函数拷贝的过程是一个字节一个字节的拷贝的,所以实际操作的时候要把void*强制转化为char*,这样在指针加的时候才会保证每次加一个字节
//(3)如果初始化的时候:
     char dest[1024] = "12345666";//{0};
     const char src[5] = "3333";
     如果用memcpy1(dest,src,sizeof(src));printf(dest);出来是3333;
     如果用memcpy1(dest,src,4);printf(dest);出来是33335666;
     而如果传4个字符,'/0'是第五个字符,那就遇到dest[1024]'/0'结束,所以是33335666
     字符串的'/0'一定要注意!!!

11、memcmp / strcmp

  1. memcmp
    memcmp是比较内存区域buf1和buf2的前count个字节。该函数是按字节比较的
    基本原型
    int memcmp(const void *buf1, const void *buf2, unsigned int count);

主要功能
比较内存区域buf1和buf2的前count个字节。

返回值

当buf1<buf2时,返回值<0

当buf1=buf2时,返回值=0

当buf1>buf2时,返回值>0

#include<string.h>
#include<stdio.h>
int main() {
	char s1[] = "Hello";
	char s2[] = "Hello";
	int r;
	r = memcmp(&s1,&s2,strlen(s1));
	if(!r)//!r 非零返回的是 1  这个是非运算,计算机是二进制的,不是零就是一了 
		printf("s1 and s2 are identical\n");/*s1等于s2*/
	else if(r<0)
		printf("s1 is less than s2\n");/*s1小于s2*/
	else
		printf("s1 is greater than s2\n");/*s1大于s2*/
//	printf("%d\n",!r); //输出是一, 
//	printf("%d\n",r);
	return 0;
}

说明:

该函数是按字节比较的。

例如:

s1,s2为字符串时候memcmp(s1,s2,1)就是比较s1和s2的第一个字节的ascII码值;

memcmp(s1,s2,n)就是比较s1和s2的前n个字节的ascII码值;

如:char *s1=“abc”;

char *s2=“acd”;

int r=memcmp(s1,s2,3);

就是比较s1和s2的前3个字节,第一个字节相等,第二个字节比较中大小已经确定,不必继续比较第三字节了所以r=-1.

  1. strcmp
    strcmp函数语法为“int strcmp(char *str1,char *str2)”,其作用是比较字符串str1和str2是否相同,如果相同则返回0,如果不同,前者大于后者则返回1,否则返回-1。

简单示例:

char a[]=“abcd”;
char *b=“abcd”;
char *d=“abcde”;
int d=strcmp(a,b); //那么d的值是0
d=strcmp(b,d); //d的值是-1 因为 ‘\0’ 比’e’ 小
d=strcmp(d,b); //d的值是1,因为 ‘e’ 比’\0’大

11.1memcpy和strcpy区别

strcpy和memcpy主要有以下3方面的区别。

  1. 复制的内容不同。strcpy只能复制字符串,而memcpy可以复制任意内容,例如字符数组整型、结构体、类等。
  2. 复制的方法不同。strcpy不需要指定长度,它遇到被复制字符的串结束符”\0”才结束,所以容易溢出。memcpy则是根据其第3个参数决定复制的长度。
  3. 用途不同。通常在复制字符串时用strcpy,而需要复制其他类型数据时则一般用memcpy
  • strcpy
    函数原型:char * strcpy(char * dest,char * src);

要求:src和dest所指内存区域不可以重叠且dest必须有足够的空间来容纳src的字符串。

11.2、memmove

使用 memmove 而不是 memcpy 的原因是 memmove 安全处理内存重叠的情况,而 memcpy 在源和目标内存区域重叠时可能会导致未定义的行为。在追加数据到缓冲区的场景中,内存重叠是可能发生的,因此使用 memmove 是更安全的选择。

void *memmove(void *dest, const void *src, size_t count);

memmove(r->send_buf + r->send_len, src, count);

12、assert() 断言

原型定义:

void assert( int expression );

assert宏的原型定义在<assert.h>中,其作用是先计算表达式 expression ,如果expression的值为假(即为0),那么它先向stderr打印一条出错信息,然后通过调用abort 来终止程序运行。

下面来看看一段代码:


#include <stdio.h>
#include <assert.h>

int main( void )
{
      int i;
   i=1;
   assert(i++);
   printf("%d\n",i);

       return 0;
}

运行结果为:
看看运行结果,因为我们给定的i初始值为1,所以使用assert(i++);语句的时候不会出现错误,进而执行了i++,所以其后的打印语句输出值为2。如果我们把i的初始值改为0,那么就回出现如下错误。
Assertion failed: i++, file E:\fdsa\assert2.cpp, line 8
Press any key to continue

是不是发现根据提示很快就能定位出错点呢

13、((void)0)

在C语言中,表达式 ((void)0) 通常用于以下几个方面:

避免编译器警告:
当你在函数中有一个未使用的返回值时,你可以使用 ((void)0) 来避免编译器生成未使用返回值的警告。这相当于明确告诉编译器你有意忽略返回值。

int foo() {
    // ... do something ...
    return 0; // 返回值可能未使用
}

void bar() {
    foo(); // 调用foo,但不关心返回值
    (void)0; // 避免编译器警告
}

一、 [C语言中关于排序的库函数]

qsort():快速排序

参考CSDN

  1. 函数原型
#iclude<stdlib.h>
void qsort(void*, size_t, size_t, int(*)(const void*, const void*))

第一个参数为待排序数组首地址。
可直接输入待排序数组名,或是指向数组的指针。
第二个参数为数组长度。
size_t是标准C库中定义的,应为unsigned int,在64位系统中为 long unsigned int。
可以直接输入待排序的数组的长度。
第三个参数为数组元素所占字节。
可直接用sizeof(a[0])计算字数组单个元素的节数。
第四个参数为所调用函数的指针,函数名即是函数的指针,可直接写函数名,调用函数用来确定排序的方式。

  1. 第四个参数(传一个函数)
    先以整型递增排序为例
int inc(const void*a, const void*b){
	return*(int*)a - *(int*)b;
	}
  1. int inc 表示函数返回一个int值。inc为函数名 ,表示递增排序(increase),也可以自己命名。
  2. ( const void * a, const void * b)将两个要对比的元素的地址传入函数。 (加const表示无法改变指针指向的值)
  3. return * ( int * )a - * ( int * ) b ,返回一个整型数,表示两个元素对比的结果。如果a大于b,则返回正数。a小于b,则返回负数。如果a等于b,则返回零。(int *)表示将地址强制类型转换成整形地址类型,可根据排序对象选择指针类型的转换。也可以改变算式,例如用 return strlen((char * )a) > strlen((char * )b) ? 1 : -1; 可以返回比较字符串长度的结果,用来根据字符串长度进行排序, 下面有相关的代码。
  • 例题:
  • 剑指 Offer 45. 把数组排成最小的数
    这里给出了一个新型的第四个参数
int compare(const void* a, const void* b){      //这里给出了一个新型的第四个参数
    char num1[20];
    char num2[20];

    sprintf(num1, "%d%d", *(int*)a, *(int*)b);
    sprintf(num2, "%d%d", *(int*)b, *(int*)a);
    return strcmp(num1, num2);
}

char* minNumber(int* nums, int numsSize){
    char *ret = (char*)malloc(sizeof(char)*1000);

    char* p = ret;   //用来做这片内存的头部指针

    qsort(nums, numsSize, sizeof(int), compare);
    for(int i = 0; i < numsSize; i++)
    ret += sprintf(ret, "%d", nums[i]);  //sprintf正常会返回打印的数量
    *ret = '\0';
    return p;
}
  1. 各种数据类型的升序排序函数
  • 如果要降序排序,只需将return里的a,b反过来写即可。
    1、整型
int inc (const void * a,const void *b)
 {
return *(int *)a - *(int *)b;
}

2、double型

int inc (const void * a, const void * b)
{
return *(double *)a > *(double *)b ? 1 : -1;
}
注: 这里两个浮点数相减但要返回一个整型数,如果按上面做法直接减会丢失小数点部分。所以需另加处理,直接判断大小,如果a大于b,则返回1,否则返回-1

3、字符排序

int inc(const void *a,const void *b)
{
   return *(char *)a - *(char *)b;
}

  1. 力扣例题
    剑指 Offer 39. 数组中出现次数超过一半的数字
int increase(const void* a, const void* b){  //直接写(int* a, int* b)也可
    return *(int*)a - *(int*)b;  //都去掉(int*)也可
}
int majorityElement(int* nums, int numsSize){
    qsort(nums, numsSize, sizeof(int), increase);  //采用C库函数,快速排序
    return nums[numsSize / 2];
}
#include <stdio.h>
#include <math.h>
int main()
{
	double x;
	while (scanf("%lf", &x) != EOF)
	{
		double temp = fabs(x);
		printf("%.3lf\n", temp);
	}
	return 0;
}

在这里插入图片描述

二、[C语言常用库函数]

  1. 列表参考CSDN
    C语言函数库:
    C语言的常用的标准头文件有 :
    <ctype.h>   <time.h>   <stdio.h>
    <stdlib.h>   <math.h>   <string.h>

1. abs()

头文件: #include <stdlib.h>
作用:求int类型数的绝对值
声明: int abs(int x);
参数: x 要 求绝对值的int型数据
返回值:int 类型, x 的绝对值

2. fabs()

头文件: #include <math.h>
作用:求double 类型数据的绝对值
声明: double fabs(double x);
参数: x 要 求绝对值的double型数据
返回值:double 类型, x 的绝对值

#include <stdio.h>
#include <math.h>
int main()
{
	double x;
	while (scanf("%lf", &x) != EOF)
	{
		double temp = fabs(x);
		printf("%.3lf\n", temp);
	}
	return 0;
}

在这里插入图片描述

3. fmax(a,b)

返回a,b中的最大值;

4. fmin(a,b)

返回a,b中的最小值;

5. sqrt() 开根号

开根号的函数

#include<stdio.h>
#include<math.h>
int main()
{
	int x = 9, y;
	y = sqrt(x);  //计算x开根号
	printf("%d\n",y);
}

3、LINUX C 高级

1.经典传参

  • 输入型参数 / 输出型参数

1.经典的传值方式,用出参传值,返回值传函数是否成功运行
2.形参是常数一定是输入型参数,是指针那么输入输出型都有可能
3.对于指针型参数:
通常输入型参数在函数内部只需读取这个参数而不需要更改他,就在指针前面加const,
如果未加const一般认为这个指针形参做输出型参数;

#include <stdio.h>

int func1(const char*p)//形参是常量指针可以放心传参,不用担心参数的值会被程序改变
{
    printf("\np = %s",p);
}

int func2(char*p)//这种传值方式要小心,有时会不成功
{
     printf("\np = %s",p);
}

int func3(int a, int *p)//经典的传值方式,用出参传值,返回值传函数是否成功运行
{        //形参是常数一定是输入型参数,是指针那么输入输出型都有可能
		//因为传入指针才能通过最后解引用的方式去更改下文主函数中的b值。
    *p = a;//传了a的值给调用该函数的函数
    if(*p  > 0)
    return 0;
    else
    return -1;
}


int main(void)
{
    int a = 3;
    int b = 0;
    int ret = -1;
    
   char*p = "LINUX";
   char p1[] = "LINUX!";
   func1(p);
   func1(p1);
   func2(p);
   func2(p1);

   ret = func3(a, &b);
   if(ret < 0){
   printf("\nFUNC_3 ERROR!");
   printf("\nFUNC_3顺利执行");
   printf("\n被传参后 b = %d",b);
}

    return 0;
}

4、算法积累------------------------

1.数组翻转

输入不定长数组(第一行,各元素用空格隔开)和数组个数(第二行),将数组元素翻转之后输出。

#include <stdio.h>

int main() {
    int a[100];
    int length=0;
    int n;
    int temp;
    //输入数组和长度
    printf("请输入数组和长度:\n");
    do{
        scanf("%d",&a[length++]);    //do和while配合取不定长数组很好用!!!
    }while(getchar()!='\n');  //scanf()和getchar()会轮流取值,见下例;
    scanf("%d",&n);
    //输入的数组
    printf("输入的数组为: \n");
    for(int i=0;i<n;i++){
        printf("%d ",a[i]);
    }
    printf("\n 输入完成!\n");
    //执行翻转
    for(int i=0;i<n/2;i++){
        temp=a[i];
        a[i]=a[n-i-1];
        a[n-i-1]=temp;
    }
    //打印出数组
    printf("翻转后的数组为: \n");
    for(int i=0;i<n;i++){
        printf("%d ",a[i]);
    }

    return 0;
}

在这里插入图片描述
法2:翻转数组的时候从尾到头打印。

2. scanf() / getchar()对比实验

注:scanf()和getchar()会轮流取值

#include <stdio.h>
int main(){
    int a[100];
    char b[100];
    int i=0,j=0,n,m;
    while((b[j++]=getchar()) !='\n'){
scanf("%d",&a[i++]);
    n=i;
    m=j;
    }
    printf("scanf()取得a[]= ");
    for(int i=0;i<n;i++){
        printf("%d ",a[i]);
    }
    printf("\ngetchar()取得b[]= ");
    for(int j=0;j<m;j++){
        printf("%c ",b[j]);
    }
    return 0;
}

在这里插入图片描述

4.冒泡排序、快速排序、选择排序、堆排序等,

嵌入入式算法题主要考察,字符串、数组、排序、链表,把输入输出、字符串、数组的操作搞清楚先。

5. C 语言的条条框框

1. C常见的报错

  1. Exited with error status 139
    出现错误的原因是空指针,如下:
#include <stdlib.h>	
#include<stdio.h>
	int main()
	{
	    int *m;     //默认初始化为NULL
	    printf("It' OK here.\n");
	    printf("*m = %d\n",*m);   //使用NULL指针导致segmentation fault.
	    printf("Is here OK?\n");
	    return 0;
	}

决解办法:分配内存

int *m = (int *)malloc(sizeof(int))

  1. Exited with error status 12
  2. Exited with error status 13

2. C语言标准

  1. 在C标准中全局变量要初始化
int a[1000] = {0}; //将数组初始化为零
  1. 全局变量最好拿static修饰
    这样防止在力扣中会与其他模块内全局变量重名;
    静态全局变量定义在函数体外;
static int i = 0; 
  1. 声明指针就要分配空间,不要存在空指针。
int *n = (int*)malloc(sizeof(int));
  1. 局部变量会在函数调用后释放,所以不要返回局部变量(地址)
    C++示例:
int * func()
{
	int a = 10;
	return &a;
}

int main() {

	int *p = func();

	cout << *p << endl;  //第一次可以输出成功(编译器对释放的局部变量做了一次保留)
	cout << *p << endl;  //第二次会输出失败

	system("pause");

	return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值