此博文是整理本人初入编程世界学习C语言的经历,零基础想学习C语言的可参考本文的学习方式,内容包含视频+笔记+思维导图总结,根据视频资源编写自己见解的学习笔记,再从笔记中提炼总结思维导图。笔记看得云里雾里是正常的,一是需要配合视频,二是因为写笔记时是想到哪里记到哪里,每个人侧重点不同,笔记仅供参考。
关键词
: C语言,零基础,学习途径,笔记,小甲鱼,鱼C论坛
声明:
本文作者原创,转载请附上文章出处与本文链接。
零基础适合观看B站小甲鱼C语言教学,内容生动有趣,能针对重点知识点进行详细讲解。每章都有课后作业,存放于鱼C论坛中,课后作业能编程尽量编,多捣鼓多敲代码,视频看一节笔记记一章。
看完视频,多敲代码,记好笔记,有一定的知识储备后,推荐制作一份思维导图,归纳总结所学,印象更深刻,温习时也更方便,制作思维导图工具推荐Xmind。有需要可评论私。
看视频,论坛课后作业的随笔,内容是自己的理解,想到哪里记哪里,有视频的难点重点,也有课后作业的答案和注重点,随意参考即可,按自己节奏走。
…
数组初始化的种种方式:
A. 将数组中所有元素统一初始化为某个值,可以这么写:
int a[10] = {
0}; // 将数组中所有元素初始化为 0
B. 如果是赋予不同的值,那么用逗号分隔开即可:
int a[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 0};
C. 还可以只给一部分元素赋值,未被赋值的元素自动初始化为 0:
// 表示为前边 6 个元素赋值,后边 4 个元素系统自动初始化为 0
int a[10] = {
1, 2, 3, 4, 5, 6};
D. 有时候还可以偷懒,可以只给出各个元素的值,而不指定数组的长度(因为编译器会根据值的个数自动判断数组的长度):
int a[] = {
1, 2, 3, 4, 5, 6, 7, 8, 9, 0};
E. C99 增加了一种新特性——指定初始化的元素。这样就可以只对数组中的某些指定元素进行初始化赋值,而未被赋值的元素自动初始化为 0:
// 编译的时候记得加上 –std=c99 选项
int a[10] = {[3] = 3, [5] = 5, [8] = [8]};
变长数组(VLA):
C99 标准新增变长数组(VLA,variable length array)的概念,这里的变长指的是数组的长度是在运行时才能决定,但一旦决定在数组的生命周期内就不会再变。(虽然可用malloc函数实现类似功能,但是VLA存储的区域为动态栈里)
printf("请输入字符的个数:");
scanf("%d", &n);
char a[n+1];
多维数组的地址计算:
以三维数组为基础,往后递推。
有数组A[m][n][p],计算A[i][j][k]的地址。
行优先:先把右边的填满。
偏移量为inp+j*p+k
列优先:先把左边的填满。
偏移量为knm+j*m+i
提醒:array[3][4]+5与array[3][4][5]不一样。
sizeof, sizelen
包含’\0’ 不包含’\0’
strcpy strncpy
拷贝字符串,不会自动追加’\0’
strcat strncat
拼接字符串,会自动追加’\0’
strcmp strncmp
比较字符串
二维数组的存储,套娃模式启动
32位系统,指针地址都是4个字节
64位系统,指针地址都是8个字节
避免访问未初始化的指针(野指针):
int *p; //(错误)
int a;int *p=&a; //(正确)//int* p = NULL;
指针数组(数组里存放指针)
//从符号优先级看
int *p1[5]={
“1”,“2”,“3”,“4”,“5”};
数组指针(指针指向数组)
char *array[5] = {
"FishC", "Five", "Star", "Good", "WoW"};
char *(*p)[5] = &array;
for (i = 0; i < 5; i++)
{
for (j = 0; (*p)[i][j] != '\0'; j++)
{
printf("%c ", (*p)[i][j]);
}
printf("\n");
}
//两者相同
Str[3] == *(str+3);
array 是数组第一个元素的地址,所以 array + 1 指向数组第二个元素;&array 是整个数组的地址,所以 &array + 1 指向整个数组最后的位置。
int array[10] = {
0};
int array[2][3] = {
{
0, 1, 2}, {
3, 4, 5}};
int (*p)[3] = array;
printf("**(p+1): %d\n", **(p+1)); //3
printf("**(array+1): %d\n", **(array+1)); //3
printf("array[1][0]: %d\n", array[1][0]); //3
printf("*(*(p+1)+2): %d\n", *(*(p+1)+2)); //5
printf("*(*(array+1)+2): %d\n", *(*(array+1)+2)); //5
printf("array[1][2]: %d\n", array[1][2]); //5
//一个[ ]对应* //先解析括号**(p+1)
不要对void指针进行解引用。
字符串的特殊约定(%s,str);
//指针不清楚指向哪就指向地址0(NULL)
char *ch = NULL
大端序和小端序:
大端序:低位数据放内存地址高位,高位数据放内存地址低位。
小端序:低位数据放内存地址低位,高位数据放内存地址高位。
sizeof(void *) ;
//因为指针的尺寸是与编译器的目标平台相关的。比如目标平台是 32 位的,那么 sizeof(void*) 就是 4,如果是 64 位的,那么 sizeof(void *) 就是 8,如果是 16 位的,那么就是2。
//指针数组与指向指针的指针
int *p[5]={
0};
int **pl=p;
char a[3][4]={
0};
char (*p)[4]=a;
//数组指针与二维数组(系统跨度一样(一维指针不能☞二维数组))//本质上数组名代表地址
“*” and "&"是一对。
课后测试:
判断大小端代码:
#include <stdio.h>
int main(void)
{
int num = 0x12345678;
unsigned char *p = (unsigned char *)#
if (*p == 0x78){
printf("您的机器采用小端字节序。\n");
}
else{
printf("您的机器采用大端字节序。\n");
}
printf("0x12345678 在内存中依次存放为:0x%x 0x%x 0x%x 0x%x\n", p[0], p[1], p[2], p[3]);
return 0;
}
课后测试:
可变参数
实现可变参数,需要包含一个头文件叫:<stdarg.h>。
这个头文件中有三个宏和一个类型是我们需要用到的,一个类型是 va_list,三个宏,一个是 va_start,一个是 va_arg(展开宏 va_arg 会得到当前 argptr 所引用的可选参数,也会将 argptr 移动到列表中的下一个参数。宏 va_arg 的第二个参数是刚刚被读入的参数的类型) ,还有一个是 va_end(当不再需要使用参数指针时,必须调用宏 va_end。如果想使用宏 va_start 或者宏 va_copy 来重新初始化一个之前用过的参数指针,也必须先调用宏 va_end)。这里的 va就是 variable-argument(可变参数)的缩写。//还有一个宏void va_copy(va_list dest, va_list src);
// 函数add() 计算可选参数之和
// 参数:第一个强制参数指定了可选参数的数量,可选参数为double类型
// 返回值:和值,double类型
double add(int n, ...) //int myprintf(char *format, ...)
{
//format[i]//代表参数里对应的字符
int i = 0;
double sum = 0.0;
va_list argptr;
va_start(argptr, n); // 初始化argptr
for (i = 0; i < n; ++i) {
// 对每个可选参数,读取类型为double的参数,
sum += va_arg( argptr, double); // 然后累加到sum中
}
va_end(argptr);
return sum;
}
课后测试:
0.我们说函数就是一种封装的方法,函数的设计应该遵从“一个函数仅实现一个功能”的原则,这样子我们就可以实现化繁为简的目的
#include <stdio.h>
char* myitoa(int num, char* str);
//把整型转为字符串
//参数:整型
//返回值:返回字符串数组
char* myitoa(int num)
{
char str[32];
int dec = 1;
int i = 0;
int temp;
if (num < 0){
str[i++] = '-';
num = -num;
}
temp = num;
while (temp > 9){
dec *= 10;
temp /= 10;
}
while (dec != 0){
str[i++] = num / dec + '0';
num = num % dec;
dec /= 10;
}
str[i] = '\0';
return str;
}
int main(void)
{
printf(“%s\n”,myitoa(520));
return 0;
}
1…不对,修改形参的值正常不会影响到实参,
2.可以,作用提前结束函数并返回
3.确保传参过程中指针地址里的值不会变
4.等
5.40;4
6.9
指针函数(常见)和函数指针(用得不多)(可做参数、返回值(函数类型))
指针函数 -> int *p();
函数指针 -> int (*p)(int, int);
做参数:int call(int (*fp)(int, int), int num1, int num2)
做返回值int (*select(char ch))(int, int)
//函数指针数组
double (*func_table[4])(double, double) = {add, sub, mul, divi};//都是函数
这个语句就定义了一个指向函数的指针变量 p。首先它是一个指针变量,所以要有一个“*”,即(*p);其次前面的 int 表示这个指针变量可以指向返回值类型为 int 型的函数;后面括号中的两个 int 表示这个指针变量可以指向有两个参数且都是 int 型的函数。所以合起来这个语句的意思就是:定义了一个指针变量 p,该指针变量可以指向返回值类型为 int 型,且有两个整型参数的函数。p 的类型为 int(*)(int,int)。
需要注意的是,指向函数的指针变量没有 ++ 和 – 运算。
课后测试:
课后测试:
char names[3][40];//C语言输入多个字符串得用二维数组或类似的
for (i = 0; i < 3; i++){
printf("\n请输入%d号玩家的名字:", i+1);
scanf("%s", names[i]);
}
作用域:
代码块作用域:
最常见的就是代码块作用域。所谓代码块,就是位于一对花括号之间的所有语句。
int main(void)
{
int i=888;
{
printf("7i = %d\n",i);//888
int i=444;
{
int i=222;
printf("11i = %d\n",i);//222
}
printf("13i = %d\n",i);//444
{
printf("15i = %d\n",i);//444
int i=111;
printf("17i = %d\n",i);//111
}
printf("19i = %d\n",i);//444
}
return 0;
}
文件作用域:
任何在代码块之外声明的标识符都具有文件作用域,作用范围是从它们的声明位置开始,到文件的结尾处都是可以访问的。另外,函数名也具有文件作用域,因为函数名本身也是在代码块之外。
原型作用域:
原型作用域只适用于那些在函数原型中声明的参数名。
函数作用域:
函数作用域只适用于 goto 语句的标签,作用将 goto 语句的标签限制在同一个函数内部。
链接属性:
在 C 语言中,链接属性一共有三种:
· external(外部的)-- 多个文件中声明的同名标识符表示同一个实体
· internal(内部的)-- 单个文件中声明的同名标识符表示同一个实体
· none(无)-- 声明的同名标识符被当作独立不同的实体(比如函数的局部变量,因为它们被当作独立不同的实体,所以不同函数间同名的局部变量并不会发生冲突)
默认情况下,具备文件作用域的标识符拥有 external 属性。也就是说该标识符允许跨文件访问。对于 external 属性的标识符,无论在不同文件中声明多少次,表示的都是同一个实体。
使用 static 关键字可以使得原先拥有 external 属性的标识符变为 internal 属性。这里有两点需要注意:
· 使用 static 关键字修改链接属性,只对具有文件作用域的标识符生效(对于拥有其他作用域的标识符是另一种功能)
· 链接属性只能修改一次,也就是说一旦将标识符的链接属性变为 internal,就无法变回 external 了
课后测试:
生存期:
C语言的变量拥有两种生存期
静态存储期(static storage duration)
自动存储期(automatic storage duration)
具有文件作用域的变量属于静态存储期,函数也属于静态存储期。属于静态存储期的变量在程序执行期间将一直占据存储空间,直到程序关闭才释放。
具有代码块作用域的变量一般情况下属于自动存储期。属于自动存储期的变量在代码块结束时将自动释放存储空间。
存储类型:
存储类型其实是指存储变量值的内存类型,
C语言提供了5种不同的存储类型:
auto register static extern typedef
自动变量(auto)
在代码块中声明的变量默认的存储类型就是自动变量,使用关键字auto来描述。 由于这是默认的存储类型,所以不写auto是完全没问题的。
寄存器变量(register)
将一个变量声明为寄存器变量,那么该变量就有可能被存放于CPU的寄存器中(CPU对寄存器读取存储几乎无延迟)。寄存器变量和自动变量(auto)在很多方面的是一样的,它们都拥有代码块作用域,自动存储期和空连接属性。不过这里有一点需要注意的是:当你将变量声明为寄存器变量,那么你就没办法通过取址运算符获得该变量的地址。
静态局部变量(static)
使用static来声明局部变量,那么就可以将局部变量指定为静态局部变量。static使得局部变量具有静态存储期,所以它的生存期与全局变量一样,直到程序结束才释放
static和extern
作用于文件作用域的static和extern,static关键字使得默认具有external链接属性的标识符变成internal链接属性,而extern关键字是用于告诉编译器这个变量或函数在别的地方已经定义过了,先去别的地方找找,不要急着报错。
课后测试:
地址存放顺序:
递归必须设置正确的结束条件
结束条件大致分为://递归函数无参数用外部;有参数用内部
外部结束:递归过程中设置静态局部变量进行条件判断
void test(void)
{
static int count = 10;
if(--count)
test();
}
// 内部结束:在递归函数的参数上(做文章)进行条件判断
long fact(int num)//求阶乘!,5!=1*2*3*4*5
{
long result;
if(num > 0)
result = num * fact(num-1);
else//num = 0
result = 1;
return result;
}
都要free;// #include <stdlib.h>
malloc:void *malloc(size_t size);
malloc 函数向系统申请分配 size 个字节的内存空间,并返回一个指向这块空间的指针。
memset函数可初始化malloc的申请的内存空间。
calloc:void *calloc(size_t nmemb, size_t size);
calloc 函数在内存中动态地申请 nmemb 个长度为 size 的连续内存空间(即申请的总空间尺寸为 nmemb * size)
备注:calloc 函数与 malloc 函数的一个重要区别是:calloc 函数在申请完内存后,自动初始化该内存空间为零,而 malloc 函数不进行初始化操作,里边数据是随机的。
realloc:void *realloc(void *ptr, size_t size);
导致内存泄漏主要有两种情况:
· 隐式内存泄漏(即用完内存块没有及时使用free函数释放)//忘记free
· 丢失内存块地址//不能free
int main(void)
{
int *a;
int c=1;
a=(int*)malloc(sizeof(int));
a=&c; //分配给a的4个字节的空间没有办法free了
return 0;
}
课后测试
课后测试:
0.不行吧
1.str主要针对字符串;mem主要针对申请的动态内存
2.int* p=(int *)malloc(1024*sizeof(int));
memset(p, 0 ,1024*sizeof(int));
3.malloc(1024);
4.初始化静态变量时不能调用函数。static 声明的变量在程序运行过程中是始终存在的,通常在 main 函数运行之前就完成了初始化过程。但 malloc 函数的调用是在 main 函数之后进行的,所以从概念上来说,static 声明的变量不可能通过调用库函数来进行初始化。同样的道理,这个规则对于全局变量来讲也是一样的
……
static int *pi;
pi = (int *)malloc(sizeof(int));
……
此动动手对程序的抒写规范(高内聚,低耦合)以及一些小技巧
(1)剪刀1石头2布3与(2)(*3)剪刀3石头6布9//可数值相加做判断条件
7\11\6:(2)win…………
rand()%3+1;//不会包括0
矩阵用一维指针表示,申请内存时可多申请两个单位。用p[0]/p[1]装MN。
ptr = (int )realloc(ptr, (m * n + 2) sizeof(int));//方便其它函数获取MN
其它函数:
int m = ptr[0];
int n = ptr[1];
int *matrix = ptr + 2;
for (i = 0; i < m; i++){
for (j = 0; j < n; j++){
matrix[i * n + j] = num;
}
}
1、代码段:
通常是指用来存放程序执行代码的一块内存区域。这部分区域的大小在程序运行之前就已经确定,并且内存区域通常属于只读,某些架构也允许代码段为可写,即允许修改程序。在代码段中,也有可能包含一些只读的常数变量,例如字符串常量等。程序段为程序代码在内存中的映射,一个程序可以在内存中有多个副本。
2、从静态存储区域分配(BSS段、数据段)
由编译器自动分配和释放,在程序编译的时候就已经分配好内存,这块内存在程序的整个运行期间都存在,直到整个程序运行结束时才被释放,全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域,未初始化的全局变量和静态变量在相邻的另一块区域。另外文字常量区,常量字符串就是放在这里。
2(1)、BSS段
通常是指用来存放程序中未初始化的全局变量的一块内存区域。BSS段属于静态内存分配。
2(2)、数据段
通常是指用来存放程序中已初始化的全局变量的一块内存区域。数据段属于静态内存分配。
3、从动态存储区域分配(栈和堆):
3(1)、在栈上分配(动态存储区域)(只有栈是从高地址到低地址发展)
同样由编译器自动分配和释放,在函数执行时,函数内部的局部变量都可以在栈上创建,函数执行结束时,这些存储单元将则被自动释放。
需要注意的是,栈内存分配运算内置于处理器的指令集中,它的运行效率一般很高,但是分配的内存容量有限。
3(2)、从堆上分配(动态存储区域)
也称为动态内存分配,由程序员手动完成申请和释放。程序在运行的时,由程序员使用内存分配函数(如 malloc 函数)来申请内存,使用完之后再由程序员自己负责使用内存释放函数(如 free 函数)来释放内存。
需要注意的是,如果在堆上分配了内存空间,就必须及时释放它,否则将会导致运行的程序出现内存泄漏(堆内存泄漏、系统资源泄漏)等错误。
在 C 语言中,不同类型变量的存储位置和作用域也有所不同。
课后测试:
0.malloc和 realloc 函数创建一个可以存放任意长度整数的容器(数字以字符的形式存储)。
宏定义的实质:机械替换。
C语言的三大预处理功能:宏定义(#define)、文件包含(#include)、条件编译(#ifdef;#else;#endif)。
不带参数的宏定义:替换操作。
#define PI 3.14
带参数的宏定义:
C 语言允许宏定义带有参数,在宏定义中的参数称为形式参数,在宏调用中的参数称为实际参数,这点和函数有些类似。
#define MAX(x,y) (((x)>(y))?(x):(y))
课后测试:
0.用逆波兰表示法做一个四则计算器
· a - b * c + d:中缀表示法(Infix Notation),运算符在两个运算对象的中间。
· + - a * b c d:前缀表示法(Prefix Notation),运算符在运算对象的前面,又称波兰表示法。
· a b c * - d +:后缀表示法(Suffix Notation),运算符在运算对象的后面,又称为逆波兰表示法。
**C语言内联函数关键字:**现在的编译器也很聪明,就算你不写 inline,它也会自动将一些函数优化成内联函数。
inline (https://blog.csdn.net/21aspnet/article/details/6723896)
内联函数在编译层面类似于宏替换。也就是说,程序执行过程中调用内联函数不需要入栈出栈,所以效率会提高。
内联函数和宏(#definde)的区别在于,宏是由预处理器对宏进行替代,而内联函数是通过编译器控制来实现的。而且内联函数是真正的函数,只是在需要用到的时候,内联函数像宏一样的展开,所以取消了函数的参数压栈,减少了调用的开销。你可以象调用函数一样来调用内联函数,而不必担心会产生于处理宏的一些问题。(但节约时间的同时增加了空间的消耗)
#include <stdio.h>
inline int add(int x, int y);
int add(int x, int y) //or: inline int add(int x, int y){}
{
return x+y;
}
int main()
{
int i = 50;
while(i--){
printf("%d\n",add(20,10));
}
return 0;
}
// 必须先声明再使用
高级宏的小技巧:
宏的预处理运算符:# 和 ##
和 ## 是两个预处理运算符。 在带参数的宏定义中,# 运算符后面应该跟一个参数,预处理器会把这个参数转换为一个字符串。
#define STR(s) # s
…
printf(STR(Hello %s,%n\n),STR(world),520);//字符串自带””
运算符被称为记号连接运算符,可以使用它来连接多个参数。
#define TOGETHER(x,y) x ## y
…
printf(“%d\n”, TOGETHER(5,20));//520
可变参数的宏:
之前我们学习了如何让函数支持可变参数,带参数的宏定义也是可以使用可变参数的:
#define SHOWLIST(…) printf(#__VA_ARGS__)
//其中 ... 表示使用可变参数,__VA_ARGS__ 在预处理中被实际的参数集所替换。
#include <stdio.h>
#define SHOWLIST(...) printf(# __VA_ARGS__)
//#运算符后面的参数转化为字符串
int main(void)
{
SHOWLIST(FishC, 520, 3.14\n);
return 0;
}
可变参数可为NULL
#include <stdio.h>
#define PRINT(format, ...) printf(# format, ## __VA_ARGS__)
//#运算符后面的参数转化为字符串
//##拼接参数如果可变参数是空参数,## 会将 format 参数后面的逗号“吃掉”
int main(void)
{
PRINT(num = %d\n, 520);
PRINT(Hello FishC!\n);//可变参数为空
return 0;
}
课后测试:
#define FIFTH_ARG(A1, A2, A3, A4, A5, …) A5
#define ARGUMENTS(A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, …) A11
番外:C结构体节省内存和增加访问速度的方法:
struct foo7 {
char c;
struct foo7 *p;
short x;
};
// 将隐含的废液写明,形式如下:
struct foo7 {
char c; /* 1 byte */
char pad1[7]; /* 7 bytes */
struct foo7 *p; /* 8 bytes */
short x; /* 2 bytes */
char pad2[6]; /* 6 bytes */
};//24
// 重排:
struct foo8 {
struct foo8 *p; /* 8 bytes */
short x; /* 2 bytes */
char c; /* 1 byte */
char pad[5]; /* 5 bytes */
};//16
C99 增加了一种新特性:支持初始结构体的指定成员值。利用该特性,还可以不按结构体声明的成员顺序进行初始化:
struct Book book = // 要先定义Book结构体
{
.publisher = "清华大学出版社",
.price = 48.8,
.date = 20171111
};
// 注意:其它未初始化的数值型成员也将被自动初始化,其中数值型成员初始化为 0,字符型成员初始化为 '\0'。
课后测试:
接收字符串注意使用数组,别一天天的想用指针
结构体嵌套:
struct A
{
struct B b;
};
结构体数组:
struct 结构体名称
{
结构体成员;
};
struct 结构体名称 数组名[长度];
//struct Add add[3];
结构体指针://主要用来传递结构体参数
指向结构体变量的指针我们称之为结构体指针:
struct Book * pt;
这里声明的就是一个指向 Book 结构体类型的指针变量 pt。
我们知道数组名其实是指向这个数组第一个元素的地址,所以我们可以将数组名直接赋值给指针变量。
pt = add;
但注意,结构体变量不一样,结构体的变量名并不是指向该结构体的地址,所以要使用取地址运算符(&)才能获取其地址:
pt = &book;
通过结构体指针访问结构体成员有两种方法:
(结构体指针).成员名
结构体指针->成员名
第一种方法由于点号运算符(.)比指针的取值运算符()优先级要高,所以要使用小括号先对指针进行解引用,让它先变成该结构体变量,再用点运算符去访问其成员。
相比之下,第二种方法更加方便和直观。第二种方法使用的成员选择运算符(->)自身的形状就是一个箭头,箭头具有指向性,一下子就把它跟指针联系起来。
需要注意的是,两种方法在实现上是完全等价的.
注意:点号(.)只能用于结构体,而箭头(->)只能用于结构体指针,这两个就不能混淆。
课后测试:
0.(32位)224
1.不行,需要book.date.year;
2.720
struct Student stu=
{
.id=123,
.name=”sr”,
.date.year=2021,
.date.month=07,
.date.day=16,
.date.time.hour=18,
.date.time.minute=30,
.date.time.second=59
};
struct Student stu = {
123, "FishC", {
2021, 07, 16, {
16, 58, 00}}};
4.一
printf(“%d:%d:%d”,pt->date.time.hour......);
用scanf(“%s”)获取字符串,别一天天想着用while(getchar())。
相同结构体类型的变量可直接赋值:
struct Test
{
int x;
int y;
}t1, t2;
t1.x = 3;
t1.y = 4;
t2 = t1;
结构体变量可做为参数传递,和变量一样使用;
不过为了程序效率,更多的是使用结构体指针做为参数传递。
动态申请结构体:
typedef struct Book
{
char title[64];
char author[32];
float price;
struct Date date;
char publisher[64];
}Book,*pBook;
Book *library[MAX_SIZE];
library[0] = (Book *)malloc(sizeof(struct Book));
课后测试:
0.报错
1.t3错
2.未知
3.b1,b2不是指针不能作为内存申请的返回值
4.test->x=3;test->y=4;
struct Test* setTest(int x, int y)
struct Test* pt;
0.单链表对链表成员的各种有序改动更容易实现
1.存储空间连续,访问更快
对于单链表来说,随机访问中间的某一个元素,都是需要大量的跳转操作(从第一个元素开始,然后通过指针一个一个往后跳,最后抵达目标位置),而数组则完全没有这个问题(直接通过下标索引值一步到位地访问)。
2.单链表:容易有序插入删除添加。
3.报错;无限递归
4.free(temp);
5.因为main里面library没有申请内存,并且addBook函数更改了library结构体指针。
传值和传地的区别(虽然都是传值):
**的内存存储结构:
*的为:
addBook(library) 传递的是 library 指针的值,也就是把 NULL 传过去了;而 addBook(&library) 传递的是 library 指针的地址,自然,传过去的是指针的地址,那么要接住它就必须使用指向指针的指针。
课后测试:
0.(都是编程)
视频里编程思路,指针妙用。赋予指针适合的意义和指向,可清晰编程逻辑。
指针巨巨巨好用,多加注意。
编码集合:太多了,看test1_45~47.c
内存池是针对内存碎片机制进行优化的一种方法并能减少时间上的消耗。
内存池其实就是让程序额外维护一个缓存区域。
实现手段可:创建一个内存池(单链表)(规定MAX(1024),count),当需要申请内存时,先判断内存池里是否有空闲空间,优先从内存池获取空间,如果没有再malloc。
free空间时,把需要释放的空间放置于内存池中,如果内存池满空闲时就free。
编码集合:看test1_47(3).c
相比起宏定义的直接替换,typedef是对类型的封装起别名,typedef可以一次性起多个别名。
// typedef与define的不同:
typedef int INTEGER;//起别名,但新别名只能继承作用,不能继承延伸
#define INT int
int main(void)
{
unsigned INT a = -1;//INT => INTEGER会报错
printf("%d\n",a);
return 0;
}
#include <stdio.h>
// int (*ptr)[3];
typedef int (*PTR_TO_ARRAY)[3];// 数组指针
// int (*fun)(void); // int *(*array[3])(int);
typedef int (*PTR_TO_FUN)(void);// 函数指针
// void (*funA(int, void (*funB)(int)))(int)
typedef void (*PTR_TO_FUN2)(int);
PTR_TO_FUN2 fun(int, PTR_TO_FUN2);
int funA(void)
{
return 520;
}
int funB(void)
{
return 1314;
}
int funC(void)
{
return 999;
}
int main(void)
{
int str[3] = {
1, 2, 3};
PTR_TO_ARRAY ptr = &str; // 指针指向数组[3]的地址
for (int i = 0; i < 3; i++){
printf("%d\t",(*ptr)[i]);
}printf("\n");
PTR_TO_FUN pfun = &funA;
printf("%d\n",(*pfun)());
PTR_TO_FUN array[3] = {
&funA, &funB, &funC};
for (int i = 0; i < 3; i++){
printf("%d\t",(*array[i])());
}printf("\n");
return 0;
}
typedef union Lab
{
int a;//4
char chr;//2
struct Student st;//104
}lab;
int main(void)
{
struct Student stu;//sizeof == 104
lab l;
printf("%d\n",sizeof(l));//104
l.a = 10;
printf("%d\n",l.a);//10
l.chr = 'a';
printf("%d\n",l.a);//97
printf("%c\n",l.chr);//a
return 0;
}
共用体(联合体):
结构体(Struct)是一种构造类型或复杂类型,它可以包含多个类型不同的成员。在C语言中,还有另外一种和结构体非常类似的语法,叫做共用体(Union),它的定义格式为:
union 共用体名{
成员列表
};
结构体和共用体的区别在于:结构体的各个成员会占用不同的内存,互相之间没有影响;而共用体的所有成员占用同一段内存,修改一个成员会影响其余所有成员。
union data
{
int n;
char ch;
double f;
}a, b, c;
// 共用体 data 中,成员 f 占用的内存最多,为 8 个字节,所以 data 类型的变量(也就是 a、b、c)也占用 8 个字节的内存,并且需要考虑数据对齐问题。
enum Week {
sun, mon, tues, wed, thur, fri, sat};//0~6
enum Week {
sun = 1, mon, tues, wed = 7, thur, fri, sat};//12378910
枚举值定义后变为常量,不能在程序中用赋值语句再对它赋值。
枚举变量:
enum Week today ?= int today;//实验多次感觉没区别
位域、位段、位字段都是一种东西。
使用位域的做法是在结构体定义时,在结构体成员后面使用冒号(:)和数字(不能超过所依附的数据类型位数的长度)来表示该成员所占的位数。
typedef struct Student//位域结构
{
unsigned int a:1;
unsigned int b:1;
unsigned int c:2;//33报错,because:sizeof(unsigned int)==4;4*8==32
}stu;
stu st;
printf("%d\n",sizeof(st));//实为1,数据对齐变4
st.a = 1;
st.b = 0;
st.c = 3;
printf("%d,%d,%d\n",st.a,st.b,st.c);//1,0,3
无名位域:
无名位域的目的只有一个,就是为了进行填充和对齐。因为某些协议中规定了某些位是保留位,暂时不使用,两个需要使用的位之间如果有几个bit是不使用的,就需要通过无名域进行调整。
也可将其前后的两个位域或成员分开放在两个字中。
typedef struct Student
{
unsigned int a:1;
unsigned int :7;//1字节
unsigned int b:5;
unsigned int :3;//1字节
unsigned int d:16;//2字节
}stu;
不能用来指定位数的类型:
如果struct成员是指针变量不能用来指定所占的位数;struct成员是double或float类型,也不能指定位数,否则编译出错,位域类型无效。
C语言并没有规定一个字节的尺寸(1字节不一定等于8位,需要看编译器。不过基本都是8位,少于8位的需要到博物馆寻找)
(32/64位系统是看size_t是多少个字节)(拓展:在同一时间中处理二进制的位数叫字长。32位系统的字长为32;64为64)
逻辑位运算符(除~其它运算符可结合=:&=;^= ;|=)
~:按位取反 优先级高
&:按位与 中
^:按位异或 低
|:按位或 最低
移位运算符(可结合=:<<=;>>=)186
(左移)<<:位移上,10111010 << 2 => 0010 1110 1000 数值上,*2^2
(右移)>>:位移上,10111010 >> 2 => 00101110 数值上,/2^2(取值与int类似不计余数)
位操作应用(突出掩码)
掩码:掩码是一串二进制代码对目标字段进行位与运算,屏蔽当前的输入位。
将源码与掩码经过按位运算或逻辑运算得出新的操作数。其中要用到按位运算如|运算和&运算。用于如将ASCII码中大写字母改作小写字母。 如A的ASCII码值为65= (01000001)2,a的ASCII码值为97=(01100001)2,要想把大写字母A转化为小写字母只需要将A的ASCII码与(00100000)2进行或运算就可以得到小写字母a。(小变大异或32)
1表示通电,0表示断开
拓展:
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
int a = 12;
printf("八进制 --> %o\n", a);
printf("十六进制 --> %X\n", a);
printf("十进制 --> %d\n", a);
char s[10];
itoa(a, s, 2);
printf("二进制 --> %s\n", s);
itoa(a, s, 3);
printf("三进制 --> %s\n", s);
return 0;
}
主要为各种文件操作函数:
#define MAX 1024
int main(int argc, char **argv)
{
// 成功返回一个指向文件结构的指针,失败null,并置位errno
FILE *p1;
if((p1 = fopen("./lab1.txt", "r+")) == NULL){
perror("打开文件lab1.txt失败!");
exit(EXIT_FAILURE);
}
FILE* p2 = fopen("./lab2.txt", "r+");
// 成功返回读到/写入的字节数。失败返回0
char buf[MAX] = {
0};// 缓冲区
int fread_ret = fread(buf, sizeof(char), MAX, p1);
int fwrite_ret =fwrite(buf, sizeof(char), fread_ret, p2);
printf("%d\n",fwrite_ret);
// 改变文件流,成功0;失败-1
fseek(p1, 0, SEEK_SET);
int i = 40;
fprintf(p1, "succes,%d\n",i);
fclose(p1);
fclose(p2);
return 0;
}
fprintf( ) 函数中格式化的规定与printf( ) 函数相同,所不同的只是 fprintf()函数是向文件中写入。而printf()是向屏幕输出。
fopen f-磁盘操纵
一般用fopen打开普通文件,用open打开设备文件
fopen是标准c里的,而open是linux的系统调用.他们的层次不同.fopen可移植,open不能.
我认为fopen和open最主要的区别是fopen在用户态下就有了缓存,在进行read和write的时候减少了用户态和内核态的切换,而open则每次都需要进行内核态和用户态的切换;表现为,如果顺序访问文件,fopen系列的函数要比直接调用open系列快;如果随机访问文件open要比fopen快。
来自论坛的经典回答:open、fopen:
前者属于低级IO,后者是高级IO。
前者返回一个文件描述符(用户程序区的),后者返回一个文件指针。
前者无缓冲,后者有缓冲。
前者与 read, write 等配合使用, 后者与 fread, fwrite等配合使用。
后者是在前者的基础上扩充而来的,在大多数情况下,用后者。
https://blog.csdn.net/lyj2014211626/article/details/71844122
各个操作系统对一些特殊字符的表示不尽相同,比如 C 语言是使用 \n 表示换行符,在 Windows 上换行符却是用 \r\n 来表示,而 Mac 上则是 \r,只有 UNIX 系统的换行符跟 C 语言一致。所以如果在 Windows 中打开一个文本文件(文本流),系统将 \r\n 自动转换为 \n(以满足 C 标准),而如果是写入文本文件,则将 \n 有转换成 \r\n 来存放。
还有一个是二进制流,相比起文本流来说,二进制流是“透明的”记录内部数据,从二进制流读取的数据始终等于之前写入到该流的数据,不会做任何自动的转换。
想要把数值写入文件中并正常查看:
需要把数值(二进制)转字符串(文本),并写入到文件中,才能在文件中正常显示,否则直接写入显示的为二进制模式(需要用xxd命令看(1、查看数据的二进制;2、查看字符串的ASCLL码)(并注意大小端显示问题)(xxd file.txt))。
ftell:返回文件光标的位置。
可移植性问题:
对于以二进制模式打开的文件,fseek函数在某些操作系统中可能不支持SEEK_END位置。
对于以文本模式打开的文件,fseek函数的whence参数只能取SEEK_SET才是有意义的,并且传递给offset参数的值要么是0, 要么是上一次对同个文件调用ftell函数获得的返回值。
标准流:
文件三个标准流(终端):标准输入(stdin)、标准输出(stdout)、标准错误输出(stderr);(这些都可以当作FILE*用)
ferror:检测文件的错误指示器(例如对一个以只读的方式打开的文件写入数据时,就会设置错误指示器)是否被设置。(只能检测是否出错,不能获取原因)(大多数系统函数会把错误原因记录在errno中)(#include <errno.h>)(更多的时候用perror函数或用strerror函数返回错误码信息)
clearerr:清除指定文件的末尾指示器(feof)和错误指示器(ferror)的状态。
拓展:
重定向:(可组合使用)
IO缓冲区:
Linux 的标准函数库中,有一种被称作“缓冲 I/O”的操作,其特征就是对应每一个打开的文件,在内存中都有一片缓冲区。每次读文件时,会连续的读出若干条记录,这样在下次读文件时就可以直接从内存的缓冲区读取;同样,每次写文件的时候也仅仅是写入内存的缓冲区,等满足了一定的条件(如达到了一定数量或遇到特定字符等),再将缓冲区中的内容一次性写入文件。
标准IO提供的三种类型的缓冲模式:
// setvbuf:用于指定一个数据流的缓存模式。
// fflush:将缓冲区内的数据强制写入指定的文件中。
#include <stdio.h>
#include <string.h>
#define MAX 1024
int main(void)
{
char buff[MAX];
memset(buff, '\0', MAX);
setvbuf(stderr, buff, _IOFBF, MAX);//_IONBF
fprintf(stderr, "welcome to here\n");
fflush(stderr);
fprintf(stderr, "hello world\n");
getchar();
return 0;
}
文章浏览阅读2w次,点赞51次,收藏33次。一、需求给定一个整数,返回大于等于该整数的最小2次幂(2的乘方)。例: 输入 输出 -1 1 1 1 3 4 9 16 15 16二、分析当遇到这个需求的时候,我们可能会很容易想到一个"笨"办法:..._整数 最小的2的几次方
文章浏览阅读865次。选项,以防止命令将 IP 地址解析为主机名。如果只想在命令的输出中显示 unix套接字 连接,可以使用。不带任何选项,用来显示已建立连接的所有套接字的列表。如果只想在命令的输出中显示 tcp 连接,可以使用。如果只想在命令的输出中显示 udp 连接,可以使用。如果不想将ip地址解析为主机名称,可以使用。如果要取消命令输出中的标题行,可以使用。如果只想显示被侦听的套接字,可以使用。如果只想显示ipv4侦听的,可以使用。如果只想显示ipv6侦听的,可以使用。_ss@,,x,, 0
文章浏览阅读568次。CommandNotFoundError: 'activate'_commandnotfounderror: 'activate
文章浏览阅读426次,点赞10次,收藏19次。完成以上步骤后,您已在 Windows 10 上成功安装并验证了 Apache Kafka。在生产环境中,通常会将 Kafka 与外部 ZooKeeper 集群配合使用,并考虑配置安全、监控、持久化存储等高级特性。在生产者窗口中输入一些文本消息,然后按 Enter 发送。ZooKeeper 会在新窗口中运行。在另一个命令提示符窗口中,同样切换到 Kafka 的。Kafka 服务器将在新窗口中运行。在新的命令提示符窗口中,切换到 Kafka 的。,应显示已安装的 Java 版本信息。_win10安装部署kafka
文章浏览阅读1.4w次。缓冲区对象(Buffer Object)是在OpenGL中用于存储和管理数据的一种机制。缓冲区对象可以存储各种类型的数据,例如顶点、纹理坐标、颜色等。在渲染过程中,缓冲区对象中存储的数据可以被复制到渲染管线的不同阶段中,例如顶点着色器、几何着色器和片段着色器等,以完成渲染操作。相比传统的CPU访问内存,缓冲区对象的数据存储和管理更加高效,能够提高OpenGL应用的性能表现。_js 缓冲数据 new float32array
文章浏览阅读912次。(1)图(Graph):图是数学和计算机科学中的一个抽象概念,它由一组节点(顶点)和连接这些节点的边组成。图可以是有向的(有方向的,边有箭头表示方向)或无向的(没有方向的,边没有箭头表示方向)。图用于表示各种关系,如社交网络、电路、地图、组织结构等。(2)网络(Network):网络是一个更广泛的概念,可以包括各种不同类型的连接元素,不仅仅是图中的节点和边。网络可以包括节点、边、连接线、路由器、服务器、通信协议等多种组成部分。网络的概念在各个领域都有应用,包括计算机网络、社交网络、电力网络、交通网络等。_图论与网络优化数学建模
文章浏览阅读1.5k次。我们经常会碰见 正在加载中,加载出错, “暂无商品”等一系列的相似的布局,因为我们有很多请求网络数据的页面,我们不可能每一个页面都写几个“正在加载中”等布局吧,这时候将这些状态的布局封装在一起就很有必要了。我们可以将这些封装为一个自定布局,然后每次操作该自定义类的方法就行了。 首先一般来说,从服务器拉去数据之前都是“正在加载”页面, 加载成功之后“正在加载”页面消失,展示数据;如果加载失败,就展示_adnroid加载数据转圈封装全屏转圈封装
文章浏览阅读1.6k次,点赞23次,收藏29次。PS: 如果执行sudo grep 'temporary password' /var/log/mysqld.log 后没有报错,也没有任何结果显示,说明默认密码为空,可以直接进行下一步(后面设置密码时直接填写新密码就行)。3.(可选)当操作系统为Alibaba Cloud Linux 3时,执行如下命令,安装MySQL所需的库文件。下面示例中,将创建新的MySQL账号,用于远程访问MySQL。2.依次运行以下命令,创建远程登录MySQL的账号,并允许远程主机使用该账号访问MySQL。_alibaba cloud linux 3
文章浏览阅读7.8k次。EXCEL中数据如何做离散性分析纠错。离散不是均值抄AVEDEV……=AVEDEV(A1:A100)算出来的是A1:A100的平均数。离散是指各项目间指标袭的离散均值(各数值的波动情况),数值较低表明项目间各指标波动幅百度小,数值高表明波动幅度较大。可以用excel中的离散公式为STDEV.P(即各指标平均离散)算出最终度离散度。excel表格函数求一组离散型数据,例如,几组C25的...用exc..._excel数据分析离散
文章浏览阅读406次,点赞7次,收藏8次。i < 5){ //第3行。int count;System.out.println ("危险!System.out.println(”真”);System.out.println(”假”);System.out.print(“姓名:”);System.out.println("无匹配");System.out.println ("安全");
文章浏览阅读3.6k次。背景测试到性能、压力时,经常需要查看磁盘、网络、内存、cpu的性能值这里简单介绍下各个指标的含义一般磁盘比较关注的就是磁盘的iops,读写速度以及%util(看磁盘是否忙碌)CPU一般比较关注,idle 空闲,有时候也查看wait (如果wait特别大往往是io这边已经达到了瓶颈)iostatiostat uses the files below to create ..._/proc/diskstat
文章浏览阅读2.4k次。问题:在Android上使用 glReadPixel 读取当前渲染数据,在若干机型(华为P9以及魅族某魅蓝手机)上读取数据失败,glGetError()没有抓到错误,但是获取到的数据有误,如果将获取到的数据保存成为图片,得到的图片为黑色。解决方法:glReadPixels实际上是从缓冲区中读取数据,如果使用了双缓冲区,则默认是从正在显示的缓冲(即前缓冲)中读取,而绘制工作是默认绘制到后缓..._glreadpixels 全黑