C语言学习笔记整理
Posted 康x呀
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C语言学习笔记整理相关的知识,希望对你有一定的参考价值。
一.数据在内存中的存储
1.1数据类型介绍
基本数据的类型:
类型的分类:
整形:
1.char也算到整型家族里面,因为字符在底层存储的时候,存储的是字符所对应的ASCII值(整数)
2.[int] 可以省略 unsigned (无符号) signed(有符号)
3.有符号signed的最高位为符号位,1表示负数,0表示正数, unsigned均为正数,最高位1是实数位,不为符号位。
浮点数类型:float double
构造类型:数组类型,结构体类型:struct 枚举类型:enum 联合类型:union
指针类型:int *pi,char *pc,float *pf,void *pv
空类型:void表示空类型,通常适用于函数的返回类型,函数的参数,指针类型。
1.2 整形在内存中的存储
计算机中的有符号数有三种表示方法,即原码、反码和补码。
三种表示方法均有符号位和数值位两部分,符号位都是用0表示“正”,用1表示"负”,而数值位三种表示方法各不相同。
原码:直接将二进制按照正负数的形式翻译成二进制就可以。
反码:将原码的符号位不变,其他位依次按位取反就可以得到了。
补码:反码 + 1就得到补码。
整数有两种,有符号数和无符号数
有符号数-- - 符号位 + 数值位
正数 0 + 数值位
负数 1 + 数值位
举例说明:
int b = -1;
//10000000 00000000 00000000 00000001 - 原码
//11111111 11111111 11111111 11111110 - 反码
//11111111 11111111 11111111 11111111 - 补码
//ff ff ff ff - 十六进制显示形式
int a = 3;
//00000000 00000000 00000000 00000011 - 原码、反码、补码
//0000 0000 0000 0000 0000 0000 0000 0000 0000 0011
//0 0 0 0 0 0 0 0 0 3
// 00 00 00 03
对于整形来说:数据存放内存中其实存放的是补码
1.3大小端字节序介绍
大端(存储)模式:是指数据的低位保存在内存的高地址中,而数据的高位,保存在内存的低地址中;
小端(存储)模式:是指数据的低位保存在内存的低地址中,而数据的高位, , 保存在内存的高地址中。
1.4浮点型在内存中的存储解析
根据国际标准IEEE(电气和电子工程协会)754,任意一个二进制浮点数V可以表示成下面的形式:
(-1) ^ S* M * 2 ^ E
(-1) ^ s表示符号位,当s = 0,V为正数﹔当s = 1,V为负数。
M表示有效数字,大于等于1, 小于2。
2 ^ E表示指数位。
IEEE754规定 : 对于32位的浮点数,最高的1位是符号位s,接着的8位是指数E,剩下的23位为有效数字M。
对于64位的浮点数,最高的1位是符号位S,接着的11位是指数E,剩下的52位为有效数字M。
另外:IEEE754规定,在计算机内部保存M时,默认这个数的第一位总是1,因此可以被舍去,只保存后面的xxxxxx部分。比如保存1.01的时候,只保存01,等到读取的时候,再把第一位的1加上去。这样做的目的,是节省1位有效数字。以32位浮点数为例,留给M只有23位,将第一位的1舍去以后,等于可以保存24位有效数字。
二.指针详细介绍
1.指针就是个变量,用来存放地址,地址唯一标识一块内存空间。
2.指针的大小是固定的4\\8个字节(32位平台\\64位平台)
3.指针是有类型,指针的类型决定了指针的+ -整数的步长,指针解引用操作时候的权限
4.指针的运算
2.1字符指针
在指针的类型中我们知道有一种指针类型为字符指针char*;
将字符串的首地址放到指针中,通过指针可以找到该字符串(不是将字符串内容放到指针里面去)
栈区:局部变量,函数形参,函数调用
堆区:动态内存如malloc等申请使用
静态区:全局变量,static修饰的局部变量
常量区:常量字符串
常量区中的内容在整个程序的执行期间是不允许被修改的,且同一份常量字符串只会创建一份,不会重复创建存储。
// An highlighted block
#include<stdio.h>
int main()
{
char arr1[] = "abcdef";
char arr2[] = "abcdef";
char* p1 = "abcdef";
char* p2 = "abcdef";
if (arr1 == arr2)
{
printf("arr1 == arr2\\n");
}
else
{
printf("arr1 != arr2\\n");
}
if (p1 == p2)
{
printf("p1 == p2\\n");
}
else
{
printf("p1 != p2\\n");
}
return 0;
}
运行结果:arr1 != arr2 ;p1 == p2
创建数组需要开辟空间,数组arr1和arr2在内存空间所在位置是不同的,所以arr1 != arr2; char p1 = “abcdef”; char p2 = “abcdef”; "abcdef"是常量字符串,不能被修改,在内存空间所占位置固定,char * p1 = “abcdef”; 是将该常量字符串的首地址放到字符指针p1中,char* p2 = “abcdef”;
是将该常量字符串的首地址放到字符指针p2中。也就是说p1和p2存放都是常量字符串"abcdef"的首地址,所以 p1 ==p2。(注意:同样的常量字符串只会存一份,不会同时存两份,所以不会开辟不同的空间来存储。)**
const char p2 = “abcdef”; (指向常量字符串的指针最好加上const!)*
2.2指针数组
指针数组是一个存放指针的数组
例如:
int* arr1[5];
char* arr2[5];
double* arr3[5];
// An highlighted block
#include<stdio.h>
int main()
{
int a = 10;
int b = 20;
int c = 30;
int* arr[3] = { &a, &b, &c };
int i = 0;
for (i = 0; i < 3; i++)
{
printf("%d ", *arr[i]);
}
return 0;
}
运行结果:10 20 30
2.3数组指针
数组指针是指针
// An highlighted block
#include<stdio.h>
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
//int* p = arr;// 数组名是首元素地址
//数组指针 存放数组的指针变量
int(*p)[10] = &arr;//(*p)代表p是指针变量
//该指针指向了一个数组,数组10个元素,每个元素的类型是int
//如果不用括号将*p括起来,写成int* p[10],这是指针数组
return 0;
}
进一步介绍数组指针
// An highlighted block
//1:
char arr[5];
char(*pa)[5] = &arr;
//2:
char* ch[8];
char* (*pc)[8] = &ch;
//3:
int(*p)[10];
//解释: p先和*结合,说明p是一个指针变量,然后指着指向的是一个大小为10个整型的数组。所以p是一个指针,指向一个数组,叫数组指针。
//这里要注意:[]的优先级要高于*号的,所以必须加上()来保证p先和*结合。
// An highlighted block
int* p1; //p1+1 表示跳过一个int类型的长度,也就是4个字节
char* p2;//p2+1表示跳过一个char类型的长度,也就是1个字节
int(*p3)[10];//p3+1表示跳过一个具有10个整型长度的数组,也就是4*10=40个字节
对一个存放数组地址的指针进行解引用操作,找到的是这个数组,也就是这个数组的数组名,数组名这时候又表示数组首元素地址!
- (p + i):相当于拿到了一行,相当于这一行的数组名
(*p + i)[j] <===> ((p + i) + j)
// An highlighted block
#include<stdio.h>
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
int* p = arr;
int i = 0;
for (i = 0; i < 10; i++)
{
printf("%d ", *(p + i));
//方式一:通过指针找到与首元素偏移i个元素的地址,
//再对齐解引用操作,找到这个元素
printf("%d ", *(arr + i));
//方式二:既然可以将arr赋值给p,说明arr与p等价
//那么就可以直接用arr替代p进行相应的解引用操作
printf("%d ", arr[i]);
//方式三:通过数组名+[下标]访问数组元素
//即arr+[下标i]访问下标为i的元素,也就是第i+1个元素
printf("%d ", p[i]);
//方式四:既然arr与p等价,
//那么也可以直接用p+[下标]的方式访问数组的元素
//上述四种方式实际结果完全相同,实际上也可以互相转换使用
}
return 0;
}
总结:我们对一个数组指针变量进行解引用操作,比如int(*p)[10],得到的是一个数组,或者说是这个数组的数组名,而数组名又可以表示该数组首元素的地址。如果要找到该数组中的每一个元素,就需要对这个数组元素的地址进行解引用操作。简单点来说就是,对一个数组指针类型进行解引用操作,得到的还是地址,对这个地址在进行相应的解引用操作,才能得到数组中的具体的元素。
2.4数组传参和指针传参
// An highlighted block
#include <stdio.h>
void test(int arr[])//ok?
{}
void test(int arr[10]) //ok?
{}
void test(int* arr)//ok?
{}
void test2(int* arr[20])//ok?
{}
void test2(int** arr) // ok ?
{}
int main()
{
int arr[10] = { 0 };
int* arr2[20] = { 0 };
test(arr);
test2(arr2);
}
以上五种传参方式均ok
注意:一维数组传参可以传数组形式,也可以传指针形式,传数组形式的时候数组元素的个数可以不写,也可以写,传指针的时候要注意指针的类型,也就是指针指向什么类型的元素,比如说指针指向int类型元素,那么指针的类型就是int 。*
2.5函数指针
我们创建函数的时候,就会在内存中开辟一块空间,既然占用了内存空间,那就有对应的内存空间地址。
函数指针,顾名思义就是指向函数的指针。
注意:
& 函数名 和 函数名均表示函数的地址!
数组名 != &数组名
函数名 == &函数名
通过函数指针,我们可以找到函数,然后去调用这个函数。 函数指针是 &
函数名,而我们函数调用的时候可以直接使用函数名,那么这里通过函数指针调用函数也可以这样写:
2.6函数指针数组
把函数的地址存到一个数组中,那这个数组就叫函数指针数组,那函数指针的数组如何定义呢 ?
int (parr1[10])();
int parr210;
int ()() parr3[10];
parr1 先和[]结合,说明parr1是数组。
数组的内容是什么呢 ?
是int()()类型的函数指针。
// An highlighted block
#include<stdio.h>
int Add(int x, int y)
{
return x + y;
}
int Sub(int x, int y)
{
return x - y;
}
int Mul(int x, int y)
{
return x * y;
}
int Div(int x, int y)
{
return x / y;
}
int main()
{
//指针数组
//int* arr[5];
//需要一个数组,这个数组可以存放4个函数的地址---函数指针的数组
int (*pa)(int, int) = Add;
int (*p[4])(int, int) = { Add,Sub,Mul,Div };//函数指针的数组
int i = 0;
for (i = 0; i < 4; i++)
{
printf("%d\\n", (*p[i])(2, 3));
}
return 0;
}
运行结果:
2.7指向函数指针数组的指针
指向函数指针数组的指针是一个指针,指针指向一个数组,数组的元素都是函数指针
// An highlighted block
#include<stdio.h>
int Add(int x, int y)
{
return x + y;
}
int main()
{
int arr[10] = { 0 };//数组
int(*p)[10] = &arr;//数组指针
char* arr1[5] = { 0 };//字符数组
char* (*p1)[5] = &arr1;//字符数组指针
int(*p2)(int, int) = Add;//&Add;//函数指针
int(*p3[4])(int, int) = { 0 };//函数指针数组
//去掉变量名p3和数组[4]剩下的就是数组的类型 int(*)(int,int)
int(*(*p4)[4])(int, int) = &p3;//指向函数指针数组的指针
//p4是一个数组指针,指针指向的数组有4个元素
//每个元素的类型都是函数指针,函数指针类型是int(*)(int,int)
return 0;
}
2.8回调函数
回调函数就是一个通过函数指针调用的函数。
理解:如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,我们就说这个函数是回调函数。
特点:回调函数不是由该函数的实现方直接调用(其实也就是回调函数自身),而是在特定的事件或条件发生时由另外的一方调用的(另一个函数调用),用于对该事件或条件进行响应。
三.字符函数和字符串函数
C语言本身是没有字符串类型的,字符串通常放在常量字符串中或者字符数组中。
strlen
求字符串长度的算法分析:strlen接收到字符串起始位置的地址时,比较该地址处的内容是否为’\\0’,若不为’\\0’, 字符串的长度 + 1。
函数介绍:strlen
size_t strlen(const char* str);
头文件:string.h
函数名:strlen
函数参数:str,参数类型是const char* ,即需要进行求字符串长度的起始地址
函数返回类型: size_t,size_t是unsigned int的类型重定义,是无符号整型。库函数使用size_t类型可能考虑的是字符串的长度不可能是负数,所以用了无符号类型size_t。
函数功能:计算字符串的长度
strcat
如果我们要将一个字符串的内容追加到另外一个字符串的末尾空间中时,需要使用字符串拷贝-- - strcat函数
函数介绍:strcat
char* strcat(char* destination,const char* source);
头文件:string.h
函数名:strcat
函数参数:
参数1:destination, 类型:char* ,表示将字符串追加的目的地位置
参数2:source,类型:char* ,表示被追加字符串的源地址起始位置。
函数返回类型: char*,实际上就是返回destination(目的地)的起始位置
函数功能:字符串追加
// An highlighted block
#include<stdio.h>
#include<assert.h>
char* my_strcat(char* dest, const char* src)
{
assert(dest != NULL);
assert(src != NULL);
char* dest_start = dest;
//1.找到目的空间中的'\\0'
while (*dest != '\\0')//跳过不是'\\0'的字符
{
dest++;
}
//2.追加
while (*dest++ = *src++)
{
;
}
return dest_start;
}
int main()
{
char arr1[30] = "hello";
char arr2[] = "world";
my_strcat(arr1, arr2);//模拟实现strcat
printf("%s", arr1);
return 0;
}
strcmp
如果我们要比较两个字符串的是否相等,或者比较字符串大小,不能用操作符 == 来直接进行判断,而是需要用到字符串比较函数strcmp
strcmp函数进行字符串追加的算法分析:
函数介绍:strcmp int strcmp(const char* str1,const char* str2);
头文件:string.h
函数名:strcmp
函数参数:
参数1:str1, 类型:char* ,表示将进行比较的第一个字符串
参数2:参数2:str2, 类型:char* ,表示将进行比较的第二个字符串
函数返回类型: int, 返回两个字符串比较的结果
函数功能:字符串比较
// An highlighted block
#include<stdio.h>
#include<assert.h>
int my_strcmp(const char* p1, const char* p2)
{
assert(p1 && p2);
while (*p1 == *p2)
{
if (*p1 == '\\0')
{
return 0;
}
p1++;
p2++;
}
//方法一:vs实现的方式
//if (*p1 > *p2)
//{
// return 1;
//}
//else
//{
// return -1;
//}
//方法二:linux下gcc实现方式
return *p1 - *p2;
}
int main()
{
char* p1 = "abcdef";
char* p2 = "abqwt";
if (my_strcmp(p1, p2) > 0)
{
printf("%s > %s\\n", p1, p2);
}
else if (my_strcmp(p1, p2) == 0)
{
printf("%s = %s\\n", p1, p2);
}
else
{
printf("%s < %s\\n", p1, p2);
}
return 0;
}
strncpy
strncpy与strcpy相比较多了一个字母n,这个n代表的是需要拷贝字符的个数,也就是说strncpy需要关注拷贝字符的个数,而不是像strcpy那样关注’\\0’。
char* strncpy(char* destination,const char* source,size_t num);
头文件:string.h
函数名:strncpy
函数参数:
【参数1】destination,类型:char*,拷贝字符的目的地位置,即接收字符的起始位置
【参数2】source,类型:char* ,拷贝字符的源地址,即拷贝字符串的开始位置。
【参数3】num,类型size_t,拷贝字符的个数,用来控制拷贝字符的长度。
函数返回类型:char* ,返回接收字符的起始位置。
函数功能:指定个数的字符串拷贝
// An highlighted block
#include<stdio.h>
#include<assert.h>
//模拟实现strncpy
//方法一
//char* my_strncpy(char* dest, const char* src, size_t n)
//{
// char* dest_start = dest;
// while ((n > 0) && (*src != '\\0'))
// {
// *dest = *src;
// dest++;
// src++;
// n--;
// }
// while (n > 0)
// {
// *dest = '\\0';
// dest++;
// n--;
// }
// return dest_start;
//}
//方法二
char* my_strncpy(char* dest, const char* src, size_t count)
//count比n更有实际意义
{
assert(dest != NULL);//引用断言
assert(src != NULL);
char* start = dest;
while (count && (*dest++ = *src++) != '\\0')
{
count--;
}
if (count)
{
while (count--)
{
*dest++ = '\\0';
}
}
return start;
}
int main()
{
char arr1[10] = "abcdefg";
char arr2[] = "1234";
size_t len = 0;
scanf("%d", &len);
my_strncpy(arr1, arr2, len);
printf("%s", arr1);
return 0;
}
strncat
strcat函数是字符串追加,在使用的时候以src的’\\0’作为追加结束标志,因此在使用strcat来追加一个字符串数组本身的时候,会因\\0被提前覆盖而无法追加成功。
strncat在追加字符串的时候,会自动在末尾处添加字符串结束标志’\\0’。(这也是我们在追加的时候,不用关注原dest, src中’\\0’,仅需关注追加字符的个数的原因)
char* strncat(char* destination,const char* source,size_t num);
头文件:string.h
函数名:strncat
函数参数:
【参数1】destination,类型:char*,被追加字符的目的地位置,即接收追加字符的起始位置
【参数2】source,类型:char* ,追加字符的源地址,即追加字符串的开始位置。
【参数3】num,类型size_t,追加字符的个数,用来控制追加字符的长度。
函数返回类型:char* ,返回接收字符的起始位置。
函数功能:指定个数的字符串追加
具体算法图解:
// An highlighted block
#include<stdio.h>
#include<assert.h>
//方法一
//char* my_strncat以上是关于C语言学习笔记整理的主要内容,如果未能解决你的问题,请参考以下文章