个人C语言学习笔记

笔记内容来自本人学习 狄泰软件学院 唐佐林 老师的视频,相关课件截图已授权

本文章未经许可不允许以任何方式转载和复制保存

C第六课

if语句分析

1.bool型变量应直接出现与条件中,不要进行比较
2.变量和常量值比较时,常量值应该出现在比较符合左边
3.float型变量不能直接进行0值比较,需要定义精度

示例程序:

int i = 1;
if( 0 == i ){}

#define EPSINON 0.00000001

float f = 0.0;

if( (-EPSINON <= f) &&  (f <= EPSINON) ){}

对于第二点,如果常量值放在==左边,当我们漏写了一个=的时候,编译器就会帮我们报错检查出错误,常量值不能出现在=的左边;对于第三点,因为浮点型是不精确的,即使初始化为0.0,但在内存中可能是0.00000001,所以float型变量与0比较可能结果都是不等于0的

switch语句分析
1.case语句分支必须要有break,否则会导致分支重叠
2.default语句有必要加上,以处理特殊情况
3.case语句和switch语句中的值只能是整型常量或字符型常量,不能是浮点型

示例程序:

int i = 0;
switch( i )
{
  case 1:/*...*/break;
  case 2.0:/*...*/break;//error,2.0不是整型或字符型常量
  default:/*...*/break;
}

如果case 1后没有break,如果case 1成立的话,直接跳过case 2的判断,而直接执行case 2中对应的代码块,如果case 2也没有加break,那就继续向下执行,以此类推
C第十四课
单引号和双引号
1.C语言中的单引号用来表示字符字面量,代表一个整数
如’a’表示字符字面量,在内存中占1个字节,’a’+1表示’a’的ASCII码值+1,结果为’b’
2.”a”表示字符串字面量,代表字符指针,在内存中占2个字节,”a”+1表示指针运算,结果指向”a”结束符’\0’
3.C编译器接受字符和字符串的比较,但无任何意义
4.C编译器允许字符串对字符变量赋值,但会发生截断,得到错误结果

示例程序:

#include <stdio.h>

int main()
{
  char* p1 = 1;
  char* p2 =1;
  char* p3 =1;

  printf("%s,%s,%s",p1,p2,p3);
  printf(‘\n’);

  return 0;
}

以上程序编译通过,但会有警告。在运行时发生段错误。原因:p1指向地址0x00000001,p2指向地址0x00000031(‘1’的ASCII码对应的十六进制为31),printf在解析的时候,将前两个参数解析为上述两个地址,但是,内存的低地址空间不能在程序中随意访问,分界点为0x08048000,低于它的都为低地址。’\n’对应的地址为0x00000010,也会段错误

C第十六课

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
运算优先级:四则运算>位运算>逻辑运算

C第二十七课

1.数组可以这样将所有元素初始化为0:int a[5] = {0};第一个元素被赋值为0,剩余的元素由于没有手动赋值,编译器自动初始化为0
2.
3.数组名代表数组首元素的地址
4.数组的地址需要用取地址符&才能得到
5.数组首元素的地址值与数组的地址值相同,但意义不同
6.因地址是指针,上述两个地址值意义不同是因为指针的步长不同
如:
int a[5] = {0};

a的值跟&a的值相同,但是a+1得到的是a的地址向后移动1sizeof(首元素)=1sizeof(int)=4个字节那么长,而&a+1得到的是a的地址向后移动1sizeof(a)=1sizeof(int)*5=20个字节那么长

数组名的盲点
1.数组名有时候可以看做一个常量指针,但不可以把数组名等同于指针,sizeof任何一种指针结果都是4
2.数组名不包含数组的长度信息,只包含数组的起始地址和数组首元素的长度信息
3.在表达式中数组名只能做为右值使用,对应第二点
4.只有在下列场合中数组名不能看做常量指针
(1)数组名做为sizeof操作符的参数
(2)数组名做为&运算符的参数
在以上两种情况下,编译器会获取这个数字的结构、类型信息,从而得到其长度

C第二十八课

指针和数组分析

1.指针之间只支持减法运算
2.参与减法运算的指针类型必须相同,即指向同一个数组,得到下标差,这样才有意义
3.指针也可以进行关系运算(<, <=, >, >=)
4.指针关系运算的前提是同时指向同一个数组中的元素
5.任意两个指针之间的比较运算(==,!=)无限制
6.参与关系运算的指针类型必须相同,否则无意义

示例程序:

#include <stdio.h>

#define DIM(a) (sizeof(a)/sizeof(*a))

int main()
{
  char s[] = {‘h’,’e’,’l’,’l’,’o’};
  char* pBegin = s;
  char* pEnd = s + DIM(s);

  for(p = pBegin;p<pEnd;p++)
  {
    printf(%c”,*p);
}
  return 0;
}

宏定义函数DIM用来求一个数组的元素个数。pEnd指向了元素’0’的后面一个位置,在C语言里,这个位置是合法的,是个擦边球,也是个技巧,在C++的STL里常用到

C第二十九课

指针和数组分析(二)

既然数组名有时候可以当作常量指针,那么指针可不可以当作数组名来使用呢?
答:可以!

下标形式VS指针形式
a[n] <-> *(a+n) <-> *(n+a) <-> n[a]

PS:指针形式效率比下标形式高,但代码可读性和代码维护性不如下标形式

示例程序:
(一)

int a[5] = {0};
int* p = a;
for(i=0;i<5;i++)
{
  p[i] = i+1;//指针当数组名使用
}

for(i=0;i<5;i++)
{
  i[a] = i+10;//上述公式的转换,本语句是合法的,相当于a[i] = i + 10;
}

验证指针和数组名不完全等同
在ext.c文件中:

int a[5] = {1,2,3,4,5};

在test.c文件中:

#include <stdio.h>

int main()
{
  extern int a[5];

  printf(&a = %p\n”,&a);  //&a = 0x08049999(假设是这个地址)
  printf(“a = %p\n”,a);  //a = 0x08049999
  printf(*a = %d\n”,*a);  //*a = 1

/*以上结果毫无疑问,因为整个数组的地址跟数组的首元素的地址相同*/

/*但如果把代码改成如下:*/
  extern int* a;
  printf(&a = %p\n”,&a);  //&a = 0x08049999(假设是这个地址)
  printf(“a = %p\n”,a);  //a = 0x1
  printf(*a = %d\n”,*a);  //段错误

  return 0;
}

为什么把ext.c文件中的数组a在tes.c文件中声明为指针后,结果完全不同?
答:首先为什么第一个打印出来的地址值跟前面的代码一样?因为a这个标识符已经在ext.c文件中存在了,是一个数组,即有了一段内存,编译器直接将test.c文件中声明的指针a映射要该段内存。

为什么后面两个打印的跟第一种情况不一样呢?
答:因为a现在是一个指针,大小为四个字节,当执行第二个printf函数的时候因为要取a的值,所以编译器去a对应的那个内存去取四个字节出来,即取出了ext.c文件里的那个数组的第一个元素1,所以结果0x1;因为0x1是低地址,不能随便被程序访问,所以段错误

所以,指针和数组名不完全等同!

a和&a的区别
示例程序

int a[5] = {1,2,3,4,5};
int* p1 = (int*)(&a+1);
int* p2 = (int*)((int)a+1);
int* p3 = (int*)(a+1);

printf(%d,%d,%d\n”,p1[-1],p2[0],p3[1]); //5,乱码,3

a代表首元素的地址,步长为一个元素的长度;&a是整个数组的地址,步长为整个数组的长度;p1指向5后面的那个地址,下标为-1则向前退一个int大小的长度,即a[4]的起始地址,所以打印出5;在p2中,对a的地址强制转换为十进制,再+1,再强制转换为int*,即变为了十六进制表示的地址,相比于原来a的地址,仅仅向后移动了1个字节,所以打印出乱码(根据系统的大小端和a的地址可以将此乱码计算出来);p3容易理解

函数中有数组做为参数
1.void f(int a[]); <-> void f(int* a);
2.void f(int a[5]); <-> void f(int* a);

结论:数组做为函数参数时,它的长度信息不会传递过去,退化为指针,所以当定义的函数中有数组参数时,需要定义另一个参数来标识数组的大小

示例程序:

void fun1(char a[5])
{
  printf(sizeof(a) = %d\n”,sizeof(a));//4

  a = NULL;//合法
}

a退化为指针,指针大小为4;a=NULL合法说明a不是数组名而是指针,可以做为左值

C第三十课

字符数组与字符串

1.在C语言中,双引号引用的单个或多个字符是一种特殊的字面量
-存储与程序的全局只读存储区
-本质为字符数组,编译器自动在结尾加上'\0'字符
如下语句均为合法的:
(1)char ca[] = {‘H’,’e’,’l’,’l’,’o’};
(2)char sa[] = {‘H’,’e’,’l’,’l’,’o’,’\0’};
(3)char ss[] = “Hello world!”;
(4)char* str = “Hello world!”;

鲜为人知的小秘密
1.字符串字面量的本质是一个数组
2.字符串字面量可以看作常量指针
3.字符串字面量中的字符不可改变,因为存储与只读存储区
4.字符串字面量至少包括一个字符,当双引号中为空时,仍然有一个’\0’

字符串字面量
“Hello World!”是一个无名的字符数组

如下表达式均正确:
1.char b = “abc”[0]; //b的值是’a’
2.char c = (“123”+1); //c的值是’2’,”123”是常量指针
3.char t = ””; //t的值是’\0’

字符串的长度
1.字符串的长度指的是第一个’\0’字符前出现的字符个数
2.通过’\0’结束符来确定字符串的长度
3.函数strlen用于返回字符串的长度

PS:因字符串是一个字符数组,所以sizeof一个字符串即相当于求一个数组的长度,如果该字符串中有多个’\0’,此时不会因遇到’\0’而结束测量数组长度,这个strlen函数不同,strlen函数遇到第一个’\0’就结束,立刻返回长度
如:
printf(“%d\n”,sizeof(“123\0abc\0”)); //打印9,因为虽然最后的字符是’\0’,但是编译器都会自动再加上一个’\0’
printf(“%d\n”,strlen(“123\0abc\0”)); //打印3

C第三十一课

关于snprintf函数
-snprintf函数本身是可变参数函数,原型如下:
int snprintf(char* buffer,int buf_size,const char* fomart,…);

当函数只有3个参数时,如果第三个参数没有包含格式化信息(比如%s,%d等),函数调用没有问题;相反,如果第三个参数包含了格式化信息,但缺少后续对应参数,则程序行为不确定。

PS:格式化信息必须与变参个数相匹配

如:
#define STR1 “Hello,\0Guagua\0”
#define STR2 “Hello,\0%sChengzi\0”

char* src1 = STR1;
char buf1[255] = {0};

char* src2 = STR2;
char buf2[255] = {0};

snprintf(buf1,sizeof(buf1),src1);
snprintf(buf2,sizeof(buf2),src2);

snprintf函数跟printf函数类似,printf函数是把参数输出到显示器上,只不过snprintf函数是把第三个参数输出到第一个参数

STR2中有格式化信息%s,而第二个snprintf函数中只有三个参数,这样运行后结果不确定,应改为snprintf(buf2,sizeof(buf2),src2,”I Love You ”);则把”Hello,\0I Love You Chengzi\0”输出到buf2中

典型问题

#define S1 “Guagua and Chengzi”
#define S2 “Guagua and Chengzi”

if(S1 == S2)
{
  printf(“Equal\n”);
}
else
{
  printf(“Not Equal\n”);
}

不同编译器的处理结果不同,有点编译器对S1,S2做了优化,认为这两个字符串的值一样,而””操作符在这个时候比较的是S1和S2的地址,由于做了优化,所以S1S2;而有的编译器不会优化,分别给S1和S2都分配了不同地址,所以S1!=S2

结论
1.字符串之间的相等比较需要用strcmp完成,不可以直接用==进行字符串之间的比较
2.不编写依赖特殊编译器的代码

C第三十二课

数组类型
-C语言中的数组有自己特定的类型
-数组的类型由元素类型和数组大小共同决定
例:int array[5]的类型为int [5]

定义数组类型
-C语言中通过typedef为数组类型重命名
typedef type(name)[size];
例:typedef int(INT5)[5]; INT5 iarray;

数组指针
1.数组指针用于指向一个数组,而不是数组首元素
2.数组名是数组首元素的起始地址,但并不是数组的起始地址
3.通过将取地址符&作用于数组名可以得到数组的起始地址
4.可以通过数组类型定义数组指针:ArrayType* pointer
5.也可以直接定义:type(*pointer)[n];

示例程序:

typedef int(INT5)[5];
typedef float(FLOAT10)[10];
typedef char(CHAR9)[9];

INT5 a1;
float fArray[10];
FLOAT10* pf = &farray;//合法

CHAR9 cArray;
char(*pc)[9] = &cArray;//合法
char(*pcw)[4] = cArray;//error

printf(%d,%d\n”,sizeof(INT5),sizeof(a1));//20,20

cArray是个数组名,代表首元素的地址,pcw是一个数组的指针,两者指针类型不同,不能赋值,编译出错
指针数组
本质还是数组,只不过元素类型为指针

示例程序:

#define DIM(a) (sizeof(a)/sizeof(*a))   //求数组长度

int lookup_keyword(const char* key,const char* table[],const int size)
{
  int ret = -1;

  for(i=0;i<size;i++)
  {
if(strcmp(key,table[i]) == 0)
{
  ret = i;
  break;
}
}
return ret;
}

const char* keyword[] = {“Guagua”,”Chengzi”};
/*keyword实质是指针数组,每个元素是个指针,指向一个字符串常量*/

printf(%d\n”,lookup_keyword(“Chengzi”,keyword,DIM(keyword)));  //1

C第三十六课

函数类型
1.C语言中的函数有自己特定的类型
2.函数的类型由返回值,参数类型,参数个数共同决定,如int add(int i,int j)的类型为int(int,int)
3.C语言中通过typedef为函数类型重命名
typedef type name(parameter list),如typedef int f(int,int);

函数指针
1.函数指针用于指向一个函数
2.函数名是执行函数体的入口地址
3.对函数名取地址,得到的还是函数的入口地址(要与数组名区分)
4.可通过函数类型定义函数指针:FuncType* pointer;
5.也可以直接定义:type (*pointer)(parameter list);

如何使用C语言直接跳转到某个固定的地址开始执行?
答:通过函数指针即可

示例程序:

typedef int(FUNC)(int);

int test(int i){return i*i;}

void f(){}

int main()
{
  FUNC* pt = test;
  void(*pf)() = &f;

  printf(“pf = %p\n”,pf);  //0x08048400
  printf(“f = %p\n”,f);  //0x08048400
  printf(&f = %p\n”,&f);  //0x08048400

  pf();
  (*pf)();
/*上面两种调用方法都合法,第一种比较常用*/

  return 0;
}

回调函数
1.回调函数是利用函数指针实现的一种调用机制
2.回调机制中的调用者和被调函数互不依赖

示例程序:

typedef int(*Weapon)(int);

void fight(Weapon wp,int arg)
{
  int result = 0;

  result = wp(arg);

  printf(“Boss loss:%d\n”,result);
}

int knife(int n)
{
  return n*5;
}

int sword(int n)
{
  return n*10;
}

fight(knife,3);
fight(sword,5);

C第三十九课

函数调用过程
1.程序中的函数用栈来维护其内存(即活动记录),每次函数调用都对应着一个栈上的活动记录。
(1)调用函数的活动记录位于栈的中部
(2)被调函数的记录位于栈的顶部
如:
在这里插入图片描述
esp指针指向栈顶地址,ebp指向函数调用结束之后的返回地址
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
当main函数调用f函数的时候,ebp若向后回退四个字节,就指向了返回地址,即回到了esp指针上一次指向的位置,esp回退到那里;ebp若向前进四个字节,就指向了ebp上一次指向的位置,ebp回退到那里,恢复到从main函数开始运行的那个样子,然后从main函数开始继续执行下一个函数,即加载下一个函数的活动记录。
函数的返回只是修改ebp指针和esp指针的地址值,栈空间里的数据不会因为函数的返回而立即改变。

所以,为什么不能返回函数里局部变量或者局部数组呢?

因为,比如上图,在f函数返回后,虽然它以前的活动记录即那段内存空间数据没变,但是当main函数继续向下执行另一个函数的时候,栈上以前存放f函数记录的地方会被用来存放另一个函数的活动记录,所以此时返回f函数的局部数据是没有意义的,会发生错误。

在这里插入图片描述

示例程序:

#include <stdio.h>

int* g()
{
  int a[10] = {0};
  return a;
}

void f()
{
  int i = 0;
  int* pointer = g();

  for(i=0;i<10;i++)
  {
    printf(%d\n”,pointer[i]);
}
}
int main()
{
  f();
  return 0;
}

在f函数里打印的不全是10个0,因为在g函数返回后,下面将执行printf函数,g函数的活动记录内存段被printf函数覆盖,g函数原来的数据不再有效

程序中的堆
在这里插入图片描述

当系统用空闲链表法管理堆内存时,我们调用malloc函数时可能会返回大于我们所需的字节数,如上图所示。

程序中的静态存储区
1.静态存储区随着程序的运行而分配空间
2.静态存储区的生命周期直到程序运行结束
3.在程序的编译期静态存储区的大小就已经确定,程序运行时不允许静态存储区被改变
4.静态存储区主要用于保存全局变量和静态局部变量
5.静态存储区的信息最终会保存到可执行程序中

C第四十课

程序文件的一般布局
在这里插入图片描述
在这里插入图片描述
答案:是

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
注意
bss区的变量都默认初始化为0,而堆、栈上的变量如果没有显示的初始化,则为随机值,即使输出是0也是个巧合

C第四十一课

野指针
在这里插入图片描述
野指针的由来
在这里插入图片描述
基本原则
在这里插入图片描述

C第四十二课

常见内存错误

1.结构体里有指针成员,指针成员未初始化
如:struct Demo{char* p;}; struct Demo d1; d1.p[i] = 0;//段错误

2.结构体成员指针未分配足够的内存空间
如:char* p = (char*)malloc(5); strcpy(p,”123456”);//p只申请了5个字节,不足够容纳”123456”,造成内存越界

3.内存分配成功,但未初始化
如:char* p1 = (char*)malloc(5); char* p2 = (char*)malloc(5); strcpy(p1,p2);//指针会越界,因为p2没有初始化字符串,即没有’\0’结束符,那就会从p2所保存的地址一直读下去,造成内存操作越界

4.内存操作越界
PS:内存操作越界不一定会段错误。因为当我们的程序很小的时候(比如只有main函数),内存空间都用来运行main函数,就可能不会出现访问非法地址而产生段错误的情况。但是,当程序很大时,即不只有main函数访问内存空间,此时出现段错误的可能性就很大

指针操作的良好习惯
1.动态内存申请之后,应该立即检查指针值是否为NULL,防止使用NULL指针。
int* p = (int*)malloc(6);
if(p!=NULL){//…}
free§;

2.释放掉指针的空间之后,立即将指针赋值为NULL,这样可以防止产生野指针,也可以避免多次释放同一段内存空间,因free函数遇到NULL时不执行任何操作。接上述代码,p = NULL;

3.任何与内存操作相关的函数都必须带长度信息,即加一个size形参来辅助防止越界

4.malloc操作和free操作必须匹配,即要成对存在

5.尽量不要跨函数来free内存,否则可能会因在当前函数之前free过该段内存且没有将指针置为NULL而造成多次free非法内存地址而使程序崩溃

C第四十四课

函数参数的秘密
1.C语言中操作数的求值顺序不一定从左到右,其依赖于编译器的实现。
如:int k = 1; printf(“%d,%d\n”,k++,k++);//可能输出2,1也有可能是1,2

程序中的顺序点
1.程序中存在一定的顺序点
2.顺序点指的是执行过程中修改变量值的最晚时刻
3.在程序到达顺序点的时候,之前所做的一切操作必须完成

C语言中的顺序点:
1.每个完整表达式结束时,即分号处
2.&&,||,?:,以及逗号表达式的每个参数计算之后
3.函数调用时所有实参求值完成后(进入函数体之前)

示例1:

int k = 2;
k = k++  +  k++;
printf(“k = %d\n”,k);

上述代码中第二行中有=,++,+这三个操作符会改变k的值,最后的分号是顺序点。该版本的编译器内部操作顺序是:先将k的值取出来,即2,把两个2相加赋值给k,k变成了4,加号两边的两个++操作在这个过程中被悬挂着,还没有被执行,当遇到分号这个顺序点的时候两个++操作生效,k自增两次变成6。不同编译器的执行结果可能不同。

示例2:

void func(int i,int j)
{
  printf(%d,%d\n”,i,j);
}

int main()
{
  int k = 1;
  func(k++,k++);
  printf(%d\n”,k);
  return 0;
}

打印结果为
1,1
3

C第四十五课

调用约定

1.当函数调用发生时
(1)参数会传递给被调用的函数
(2)返回值会被返回给函数调用者
2.调用约定描述参数如何传递到栈中以及栈的维护方式
(1)参数传递顺序(形参都从左到右还是都从右到左入栈)
(2)调用栈清理(函数返回后清除其活动记录)
3.常用的调用约定
(1)从右到左依次入栈:_stdcall,_cdecl,_thiscall
(2)从左到右依次入栈:_pascal,_fastcall
PS:C语言编译器用_cdecl约定,即从右到左。调用约定不是C语言本身的一部分,是编译器的。

调用约定一般用在什么地方?
当我们在项目中要用到第三方库文件时,就要检查第三方库文件的调用约定跟自己的是否一致,否则会出错。

可变参数

#include <stdio.h>
#include <stdarg.h>

float average(int n,...)
{
  va_list args;
  int i = 0;
  float sum = 0;
  
  va_start(args,n);

  for(i = 0;i<n;i++)
  {
    sum += va_arg(args,int);
}

va_end(args);

return sum/n;
}

int main()
{
  printf(%f\n”,average(5,1,2,3,4,5));
  printf(%f\n”,average(4,1,2,3,4));

  return 0;
}

上述代码中,va_list定义得到一个集合名为args;va_start中的第二个参数n必须是…之前的那个,目的是为了得到那些自定义参数起始地址;va_arg命令从args集合中每次取出一个int类型的数据;va_end用来关闭args集合

可变参数的限制
1.可变参数必须从头到尾按照顺序逐个访问
2.参数列表中至少存在一个确定类型的命名参数
3.可变参数函数无法确定实际存在的参数的数量
4.可变参数函数无法确定参数的实际类型

注意:va_arg中如果指定了错误的类型,那么结果是不可预测的

C第四十六课

函数与宏

1.宏是由预处理器直接替换展开的,编译器不知道宏的存在
2.函数是由编译器直接编译的实体,调用行为由编译器决定
3.多次使用宏会导致最终可执行程序的体积增大
4.函数是跳转执行的,内存中有一份函数体存在
5.宏的效率比函数高,因为是直接展开,无调用开销
6.函数调用时会创建活动记录,效率比如宏
7.宏的定义中不能出现递归定义

宏的妙用

#define MALLOC(type,x) (type*)malloc(sizeof(type)*x)
#define FREE(p) (free(p),p = NULL)

#define LOG_STRING(s) printf(“%s = %s\n”,#s,s)

#define FREACH(i,n)  while(1){ int i = 0,l = n;for(i=0,i<l;i++)
#define BEGIN       {
#define END         }break;}

为什么要用while?

因为要保证有一个代码块出来,这样变量i和l只在该代码块中,多次使用这个宏的时候不会出现重复定义,且break保证了while只循环一次*/

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值