一.命名
1.由下划线,数字,英文字母组成;且首字符不能是数字。
2.尽量做到见名知意
3.严格区分大小写斜体样式
4.在C89中,限制标识符的长度为8,长度大于8,但是在C99中对长度没有任何限制
5.尽量 不要同时用零和O或者l和1以下是它们在编译器的样子,十分难以区分,会破坏代码的可读性
二.变量和常量
定义变量的格式 数据类型 变量名
定义:分配一块内存空间并为它起上名字,从此这块空间和这个名字同生死
声名:提前告诉编译器这个名字已经预定好了,其它变量不能 用它来做为名字
常量分为
1.字面常量
1,1.0形如这样的叫字面常量
2.#define定义的宏常量
形如 #define PI 3.14这样的,它的意思是在预编译时会把PI替换成3.14
3.const定义的常 量
在c语言中,它更切近于“变”这个词,而在c++中它更切近于常这个词。如下,在.c文件中它会报错,而在.cpp文件中它不会报错
const int a;
int const a;
这两个是等价的;
4.字符常量 :字符串常量** 字符常量的定界符为’’ 字符串常量的定界符为“”
5. 枚举标识符
enum week {
Mon = 1,
Tue = 2,
Wed = 3,
Thu = 4,
Fri = 5,
Sat = 6,
sun = 7,
};
int main()
{
enum week wk;
wk = Mon;
return 0;
}
`
(如上面的Mon)不能赋值float类型,可以赋值为负数,不同的枚举标识符也可以赋相同的值,如果没有赋值的话,那么第一个枚举标识符的默认的值为0,之后的枚举标识符的值依次加1.
6.转移字符
常见的转义字符 | ASCLL |
---|---|
\a 警告符 | 7 |
\b 退格符,将位置移到前一列 | 8 |
\f 换页符,将位置移到下一页开头 | 12 |
\n 换行符 ,将位置移到下一行开头 | 10 |
\r 回车符,将位置移到本行开头 | 13 |
\t 水平制表符,将光标往后移动一个TAB的长度 | 9 |
\v 垂直制表符 | 11 |
\ 代表一个\符号 | 92 |
’ 代表一个’符号 | 39 |
" 代表一个"符号 | 34 |
? 代表一个?符号 | 63 |
\0 空字符 | 0 |
\000 1到3位8进制的数字 | 3位八进制数 |
\xhh 1到2位16进制的数字 | 2位二进制数 |
注意点
int main()
{
char a ='\''; //存放单引号的值时,必须先转义
char b = '"';
char c = '\"'; // 存放双引号时,可以加\也可以不加
char d [] = {"\"c\"语言"};// "c"语言 这样的情况,就必须加\
return 0;
}
int main()
{
char a = 0; //0
char b = '\0'; //空字符 0
char c = ' '; //空格字符 0
char d = '0'; // 48
}
一道笔试题
int main()
{
char stra[] = {"hello\0data"};
char strb[] = {"hello\\0data"};
int size = sizeof(stra);
int len = strlen(stra);
printf("%d",size); //11 h,e,l,l,o,\0,d,a,t,a,\0依次被存入数组的空间
printf("%d",len); //5
printf("%s",stra); //hello \0是字符串结束的标志
printf("%s",strb); //hello\0data h,e,l,l,o,\,0,d,a,t,a(\\首先被转义成\)
}
如果要存路径,一定要注意
int main()
{
char path1 [] = {"c:\\c语言"};//记住是\\
char path2 [] = {"c:/c语言"};//防止忘记起见,在window 和 linux下都可以这样写
}
一道有需要注意的题
int main()
{
char a = {"c\141语言"};
printf("%s",a); // 打印结果 ca语言
char b = {"c\149语言"};
printf("%s",b);//因为8进制每位最大为8,所以它看成\14(两位8进制的数),并打印它对应的符号,而9则看成字符跟在后面打印出来
}
char c = {"c\666语言"};
printf("%s",c);//无法通过编译,因为\666所对应的10进制数已经超过ACSLL码的范围
c++中变量的定义
int a =0;
int a =(int)0;
int a(0);
这些都是一样的
三 .运算
1.操作数
操作数可以为变量,也可以为常量,
2.运算符
对数据进行操作的符号
可分为
2.1:单目运算符
具有回写能力
++,- -
a++; 后自增 a–;后自减(先赋值,再运算)
++a;先自增 --a;先自减(先运算,再赋值)
常量不能进行自增自减操作
a=b++;
先把b的值放在临时空间中(寄存器),然后把这个值赋值给a,然后这个值再加1,然后回写给b
a=++b;
先把b的值放在临时空间中(寄存器),然后这个值再加1,然后回写给b,然后这个值赋值给a.,
int main()
{
float a =3.25;
a++;//a的值为4.25
a--;//a的值为2.25
//对float/double自增,自减时,只对整数部分做运算
}
int main()
{
int i=0;
++i=100;
i++=100;//错误 先把i放到临时存储空间,然后把100赋值给这个临时量,因为常量不能被赋值,所以错误
}
2.2.双目运算符
特殊 ‘%’ c ,c++,java中取余,python中取模
取余运算在计算时向0方向舍弃小数位(遵循尽可能让商大)
5%-3=2
-5%3=-2;
取模运算在计算时向负无穷方向舍弃小数位(遵循尽可能让商小)
5%-3=-1;
-5%3=1;
在c语言中取模的数只能为整型
2.3.三目运算符
A?B:C; (if ,else if,if 的简写)
3.左值 "="左边的称为左值
左值,既可以赋值,又可以取值
5.右值 "="右边的称为右值
右值,只可以取值
2.4.逗号表达式
int main()
{
int a=1,b=2,c=3,d=0;
d=a+1,b+2,c+3;//d为2
d=(a+1,b+2,c+3);//d为6
}
int main()
{
int n;
while(scanf("%d",&n),n>0)//,逗号表达式的用法
{
代码块;
}
}
等号的优先级高于逗号表达式
3.强制类型转化
3.1.基本数据类型的强制转化
当把一个大字节的变量强制转化给一个小字节的变量时,从大字节的低位截断
3.2.地址的强制转化
把地址以不同的类型进行解析
c++中强制转化
static_cast
const_cast
reinterpret_cast
dynamic_cast
为了让使用地址强制转化时更安全,同时也为了提醒使用者,现在在进行地址的强制类型转化,比较危险,要注意
一道面试题
int main(){
const int c = 10;
int d = 0;
int* p = (int *)&c;
*p = 100;
d = c;
printf("%d\n", c);10
printf("%d\n", d);100
printf("%d\n", *p);100
}
因为c++中const 修饰的视为常量,所以在编译的时候就把代码中的c全部替换成10
运算符
- : !
- : |
- : &
- : ^
- : <<
- : >>
- : ^
1:结合律 (a^b) ^c =a ^(b ^ c)
2:交换律 a ^ b =b ^a
3:归零律 a ^ a =0
4: 恒等律 a ^0 =a
5:自反律 a ^ b ^a =b
:<< 左移时,无论正负,低位都补0
:>>右移时,高位补符号位
计算数字中1的个数
int Getnumber3(int val) {
int number = 0;
int n = sizeof(val) * 2;
for (int i = 0; i < n; i++) {
number += "\0\1\1\2\1\2\2\3\1\2\2\0\2\3\3\4"[val & 0x0f];
val = val >> 4;
}
return number;
}
int Getnumber2(int val) {
int number = 0;
while (val != 0) {
val &= (val - 1);
number++;
}
return number;
}
int Getnumber(int val) {
unsigned int t = val;
int number=0;
while (t != 0) {
if (t & 0x001 ) {
number++;
}
t = t >> 1;
}
return number;
}
四.关键字
1.sizeof
计算变量或类型的大小,(是在预处理时就进行的)
int main()
{
int a[10] = { 1,2,3 };
int b = 10;
int* p;
int size1 = sizeof(a);
int size2 = sizeof(p);
int size3 = sizeof(++b);
printf("%d\n", size1);//40 4*length
printf("%d\n", size2); //4(32位操作系统),8(64位操作系统)
printf("%d\n", size3);//4
printf("%d\n", b);//10 sizeof为关键字,在计算的时候,它不关心括号里面具体的运算,只是关心它的类型,
}
int main()
{
char a []={"cyuyan"};
int l1 = strlen(a);
int 12 = sizeof(a);
int l3 = sizeof("hellocyuyan");
printf("%d",l1);//6
printf("%d",l2);//7后面还有'\0'
printf("%d",l3);//12 后面还有'\0'
}
2.typedef 在c语言中为类型起一个别名,是存储型关键字,与auto,extern,mutable,static,register等不能出现在一个表达式中
auto:(c语言及,c++98,用于声名变量是自动类型的变量,具有自动存储期,这种变量在进入变声名该变量的程序块时才被建立,退出则会被撤销,在c++11时,它被用做类型推断,即定义变量时不需要声名类型,类型则会被推断出来)
int main()
{
struct stuent
{
char [20] name;
int age;
}Student,*PStudent;//一个struct student 类型的变量Student,一个指向struct student类型的指针PSstudent。
typedef struct student
{
char [20] name;
int age;
}Student,*PStudent;//一个包含char [20]name,int age 的类型,一个指向struct student的指针类型
}
int main()
{
typedef int array [10];
array a = {1,2,3};//a为长度为10的数组
}
3.static
修饰变量或函数(存放在数据区)
局部变量:当这个函数被调用时,这个函数中的静态变量被初始化,当下一次调用这个函数的时候,这个static变量再也不会被初始化,仍然保持上次的结果
全局变量:只能在当前c文件中使用
当static修饰局部变量时,当这个局部变量未初始化,那么它的默认值为0(而全局变量定义未初始化时,它的默认值也为0) (未初始化的全局变量存放在数据区的bass)
int main()
{
void a()
{
static int a;
}
void b()
{
static int a;
}
}
这两个并不冲突,尽管它们都存放在数据区,但是它们各自的作用域只在它们各自的函数中,因为在数据区存放的时候,它们会被标记,那个b是哪个函数里面的
void fun(int x)
{
int a = 0;
static int b = x;
a+=1;
b+=1;
printf("%d %d",a,b);
}
int main()
{
int n=5;
for(int i=n;i>0;i--)
{
fun(i);
}
}
在c语言中比不能通过编译(不能把变量给static 修饰的变量赋值),不过这个经常会当作c++的题来作为笔试题,在链接的时候,先在数据区给b开辟空间,并赋初始值0,并给它一个标志位1,当第一次调用fun函数,执行到static int b =x,先检查标志位是否为1,为1,然后把x赋值给b,然后把标记域改为0,等到下一次调用fun函数,执行到这一语句,当发现标记域为0时,对b不进行初始化
int mian()
{
int static a = 10,b = 10;
const static c = 10,d = 10;//a,b,c,d都为常量
int e = 10,static f=10;//不能和const一样,会报错
}
4. extern.
可以用来修饰变量或者函数
1.修饰同一个工程中的其它.c文件中的变量/函数
告诉编译器先进行编译生成.obj.文件,等链接时再去获取它具体的值/内容
2.修饰同一个.c文件中的变量
int main()
{
extern int a;
void n(int m,int n)
{
a=m+n;
}
int a=1;
}
当在一个工程下,其中的一个.c文件中使用static修饰了一个全局变量,就算在其它.c文件中对这个变量使用了extern关键字,也不能在其它文件中使用
void n()
{
int a;
}
int main()
{
extern int a;
}
不能这样使用,extern中针对于全局变量或者函数
5.const
5.1 const修饰变量
可以修饰全局变量,也可以修饰局部变量(修饰之后,变量只可读,不可写)
int const a = 1;
const int a = 1;
int main()
{
const int a = {1,2,3};//也可以修饰数组
}
int main()
{
const int a;//c编译中不会报错,c++编译中会报错
**加粗样式** int b = a;//在c中无法通过编译
}
所以用const修饰变量时一定要首先初始化
int main()
{
int const a = 10,b = 10;
const int c = 10,d = 10;//a,b,c,d都为const修饰的常变量(const是与类型绑定在一起的)
int e = 10,const f=10;//此时f不为常变量(const左右没有变量,被自动忽略)
}
5.2 const修饰指针**
1. int * const p;
int main()
{
int a =10;
int * const p = &a;
*p = 20;
int b;
p = &b;//ERROR
}
只能修改p所指向物的值,不能修改p的指向物,cost这样修饰时,必须在定义时就赋值
2. int const * p; 等于const int *p;
int main()
{
int a = 10;
int b = 20;
int const *p = &a;
*p=100;//ERROR
P = &b;//
}
能修改p所指向物,而不能修改p所指向物的值
3. const int * cnost p;
int main()
{
int a = 10;
int b = 20;
const int * const p = &a;
*p = 100;//ERROR
p = &b;//ERROR
}
既不能修改p的指向物,也不能修改p所指向的值
const和define在作用上很像。但也有很多不同
1.#define是预编译指令,而const是变量的定义,
2.define是在预处理时展开,而const是在编译时处理。
3. const定义的是一个变量,而define定义的是一个常量,
4. define定义的宏在预编译之后就不存在了,也没有占用空间,而const修饰的常变量本质是一个变量,它具有类型,占有空间,可以说常变量是有名字的常量,有名字是为了在程序中便于引用,从使用者的角度,它除了不能作为数组的长度(c语言中),它具有宏的优点,
所以在define和const都能使用时,首先考虑const,define定义的为常量,const定义的是变量,有数据类型,编译器会对它进行安全检查
#define SUM(x,y) x*y
int main()
{
int a=5,b=4;
printf("%d",SUM(a+2,b+2));//结果为(a+2*b+2) 15
}
边界问题
6.register
建议将变量存在cpu 的寄存器中以提高访问速度,以提高访问速度
1.不能定义过多,因为cpu中只有4个通用寄存区,而且有些寄存器只能接受特定的类型,如指针类型或者浮点类型,而且能否存放在寄存器中,还看你的编译器,有些寄存器变量会被直接忽略
2.寄存器不能用’&'符取地址,因为它存放在寄存器中
3.只有局部变量和形参才能定义为寄存器变量,全局变量不行(在程序执行时一直占用cpu的寄存器资源
4.局部静态变量不能定义为寄存器变量
五.选择结构
boolen类型
在c99引入了要想使用的话,必须引入<stdbool.h>
也可以自己定义
typedef char bool
#define ture 1
#define false 0
int main()
{
bool x=1;
printf("%d",x);//1
x=x-1;
printf("%d",x);//0
x=x-1;
printf("%d",x);//1
}
只有0,1值
1.if else if , else
2.switch
1 使用形式
if,else if,else只能控制离它最近的一条语句,如果想控制多个语句,必须加花括号。
else if,else遵循最近原则,向上找离它最近的If/if ,else if 进行匹配
使用这个语句,可以按照自己逻辑,不必约束
if(条件)
{
语句;
}
/--------------------------------------------/
if(条件)
{
语句;
}else if(条件)
{
语句;
}
/---------------------------------------------/
if(条件)
{
语句;
}else if(条件)
{
语句;
}else if(条件)
{
语句;
}
/------------------------------------------------/
if(条件)
{
语句;
}else if(条件)
{
语句;
}else
{
语句;
}
/--------------------------------------------------/
if(条件)
{
if(条件)
{
语句;
}else if
{
语句:
}
}else if(条件)
{
语句;
}
2. swith
1.只能对基本数据类型中的整型使用switch,这些类型包括int char等
2.switch()的参数类型不能是浮点型,字符串
3.case标签必须是常量表达式
4.case标签不能重复
case 4+2://ture
case 'A'//ture
case 'A'+2: //ture
case 6.5 //false
六.循环结构
1. for( ; ; ;)
这三种等价
/--------------------------------------------------/
int mian()
{
int sum = 0;
for(int i=1;i<=100;i++)
{
sum+=i;
}
}
/---------------------------------------------------/
int mian()
{
int sum = 0;
int i=1;
for(;i<=100;i++)
{
sum+=i;
}
}
/---------------------------------------------------/
int mian()
{
int sum = 0;
int i=1;
for(;i<=100;)
{
sum+=i;
i++;
}
}
/---------------------------------------------------/
int mian()
{
int sum = 0;
int i=1;
for(;;)
{
sum+=i;
i++;
}
}//死循环
/---------------------------------------------------/
int mian()
{
int sum = 0;
int i=1;
for(;;)
{
sum+=i;
i++;
if(i>=100)
break;//这个与上面的三种等价
}
}
int main()
{
for(int i=0;i<5;i++);
for(int i=0;i<4;i++);//在有的编译器中,此时i的作用域只在这个for循环中,而有的编译器则不是这样
}
2.while()
{}
2.do
{}while;先执行一遍函数体,再进行判断
使用这些的时间要小心在后面加一个’;',这样会执行这个空语句,而不去执行函数体里面的内容
跳转语句:break,continue,goto,return,
1.break:用在switch和循环中,每次只能跳一层(即只能跳出离它最近的循环结构)
2.continue
for(表达式1;表达式2;表达式3)
{
if(条件)
{
continue;//跳往表达式3
}
}
while(表达式1)
{
continue;//跳往表达式1
}
do{
continue;//跳往表达式1
}while(表达式1);
在while和do while中使用continue中要小心,因为它们里面很容易出现死循环
3.goto 语句(
尽量不要使用)
int main()
{
for(int i=0;i<=10;i++)
for(int j=0;j<=10;j++)
{
if(i==5&&j==5)
goto flag;
}
flag ://如果满足条件,则直接跳转到这里
}
int main()
{
goto flag;
for(int i=0;i<=10;i++)
for(int j=0;j<=10;j++)
{
flag :
printf("flag");
}
}//不允许直接从循环外往循环里面跳
int main()
{
int add(int x,int y)
{
flag:
return x+y;
}
int main()
{
goto flag;
}
}//也不允许从一个函数条跳往另外一个函数
七.作用域
1.局部变量
局部变量的作用域为什么只在该函数中:
代码以二进制形式存储在代码区,当执行到一个函数时,在栈区为这个函数申请栈帧,而函数中的形参和变量在这个栈帧中分配空间,而为函数分配的这些空间是相互独立的,所以这个这些变量也被称为局部变量,而它们的作用域也仅仅在该空间,也就是该函数内。
2.全局变量:全局变量储存在数据区(初始化的全局变量存放在data区,未初始化的全局变量存放在bass区,不分配空间),数据区只有在程序全部执行完之后才会被释放。所以它们的作用为整个程序。
全局变量定义后,如果不赋初始值的话,则默认值为0 这也是为什么它可以不分配空间
int add(int a,int b)
{
int c = a+b+d;//此时无法通过编译,尽管d为全局变量,储存在数据区,但是代码是储存在代码区从上往下依次执行的,当执行道这一语句时,此时还没有把d存储在数据区,所以说对于编译器来说,d还未定义
return c;
}
int d;
int main()
{
return 0;
}
3.静态变量: 当static修饰局部变量时,它只能在定义的该函数内使用。
当static修饰全局变量时,它的作用域为整个程序
八.数组
一.一维数组
1. 数组:
1.数组的元素可以是任意类型,
1.1 .数组的特征:
1.1.1.元素类型
1.1.2.元素个数
描述一个数组:一个Type类型的长度为N的数组
2.定义: 类型 名字 [N] N必须为大于0的常量
int main()
{
int n;
int a[n];
scanf("%d",&n);
}
2.计算数组长度
int length = sizeof(arr) / sizeof(arr[0]);
为什么数组定义的时候只能是常量?
因为如果是变量的话,它的长度是未知的,而在windows中栈的空间只有1M,为了防止在程序运行时从终端接受一个值,直接把栈的空间占用完,所以不允许数组的长度为变量
数组名 :被看作数组第一个元素的地址(sizeof中除外),在表达式中被自动转化成指向第一个元素的指针常量
3.arry[i] = * (arry+i)
所以arr[i] =i[arr];
int main()
{
int arry[5] = {1,2,3,4,5};
for(int i=0;i<5;i++)
{
printf("%d",a[i]);
printf("%d",i[a]);//这两个等价
}
}
4.数组做形参时会退化成指针
void print_Arry(int br[5],int n)
{
int size = sizeof(br);//为4
}
void int br[3][4]//退化成 int (*br)[4]
{
}
//
int main()
{
int arr[5] = {1,2,3,4,5};
print_Arry(arr,5);
}
此时并不会在printf_Arry这个栈帧中创建一个长度为5的数组,而是创建一个指针变量存储arr首元素的地址
二.维数组
可以理解成一个一维数组的每个单元格里面又存储了一个数组
arry 与 &arry 的加1能力不同
对于arry,这个地址为该数组的首元素的的地址
对于&arry,这个已经把整个数组看成一个整体,而向+1则加的是整个数组的字节大小
int arry [5] = {1,2,3,4,5};
arry + 1//指针往后移动4字节
&arry +1//指针往后移动20个字节
int arry[3][4] = {1,2,3,4,5,6,7,8,9,10,11,12};
int (*p)[4] = arry;
int (*p)[3][4] = &arry;
一道题
int ar[5][2] = {1,2,3,4,5,6,7,8,9,10};
int (*s)[2] = &ar[1];
int * p = ar[1];
printf("%d \n",s[2][3]);//*(*(s+2)+3)
printf("%d \n",p[3]);
柔性数组:
在c语言中,可以使用结构体产生柔性数组,数组的大小为0或者不定义,对于编译器来说,这个数组不占空间,它可以动态分配空间,但是它的数组的定义必须放在最后
用法:
struct array {
int size;
char data[];
};
int main() {
struct array* a2 = (struct array*)malloc(10);
}
此时data的大小为6个字节(10个字节减去int字节的大小)
要注意的点
struct array {
int size;
char data[];
};
int main() {
struct array a1 = { 1,2,3,4,5 };
printf("%d", sizeof(a1));//4
}
柔性数组它并不占空间,sizeof是在编译时根据类型计算大小的,所以为4
九.函数
为什么c语言不可以重载,而c++可以重载?
采用名字粉碎技术
c和c++的函数在内部是通过修饰名来识别,修饰名是编译器在编译函数后形成的字符串。
c语言的名字修饰规则特别简单,是函数在编译之后,只是在原来的函数名称前面加了" _ ",
让我们看一个反汇编代码
这个是mian函数编译后的修饰名
当同名的函数出现时,因为编译后的函数名称都相同,所以函数不能重载
而对于c++来说,
c++的规则,
1.以?标识函数名的开始,后跟函数名
2.函数名后面以"@@YA"标识参数表
3.参数表的符号表示
X: void
D : char
E:unsigned char
F:short
H:int
I:unsigned int
j:long
K: unsigned long
M:float
N:double
_N:bool
PA:表示指针,后面的代号表明指针类型,如果相同类型的指针连续出现,以"0"代替,一个0代表一个重复
4.参数的第一项为该函数的返回类型,其后依次跟参数的数据类型,指针标识在其所指的数据类型前
5.参数表后以"@Z"标识整个名字的结束,如果没有参数,则以Z结束
int add(int a,int b){
return a+b;
}
它的修饰名为 (?add@@YAHHH@Z)
1.库函数 用户只需要在自己的程序中引入头文件,就可以使用该头文件中的函数
2.自定义函数 函数返回值类型 + 函数名+形参列表+函数体
函数的调用前面必须有函数的定义或者函数的声名,因为代码是从上往下依次执行的
main函数的三种形式
- int main(){
return 0;
}
2. int main(int argc,char *argv[]){
return 0;
}
3. int main(int argc,char * argv[],char *envp[]){
return 0;
}
argc是argv中字符串的个数
argv可以通过命令行来传递参数
envp存放环境变量
传进来的字符串存储在为主函数分配的栈帧的下面(详情请等待我之后的博客)
函数的声名
int add(int a,int b);
int add(int ,int);//可以只写类型
int add(int a,int b);
int mina()
{
int x=2,y=1,z;
z=add(x,y);//函数的调用
z=add(int x,int y);//false 不可以这样写
}
int add(int a,int b);
int main()
{
add;//函数的地址
}
4.如果函数有返回值的话函数的返回值,那么它的返回值是通过寄存器把这个值带回去的。
5.return 终止函数,如果是函数中,则返回调用者,如果在mian函数中,则返回给操作系统
如果返回值为void ,则return 可以不写,也可以这样写: return; 在有的编译器中也可以这样写 return void;
6.exit,直接终止程序
1.exit(0);程序正常结束 和 exit(EXIT_SUCESS);等价
2.exit(1);程序异常结束 和 exit(EXIT_FAILURE);等价
十.指针
1.指针的简介及用法
%p 控制打印地址值
32位操作系统指针为4字节
64位操作系统指针为8字节
类型对指针的作用
1:指针加1的能力
2:指针向下解析的大小
int mian()
{
int a;
printf("%p",&a);
return 0;
}
*的用法
int mian()
{
int a=1,b=1;
b=a*5;//乘法
int *p;//定义
int *p=&a;
*p=5;//解引用
}
2.存放模式
小端模式:高位存放在高地址
大端模式:高位存放在低地址
c语言中按小端模式存放
请看下面的图片;7b相比于f8为高位,所以在第二张图片中,7b存放在高地址
指针为什么要有类型?
指针存的是变量的首地址,而解析时,找到该地址,然后看该指针的类型,如果是Int类型,则向下解析4个字节
所以假如把int 类型的变量的地址赋值给float类型的指针时,当解析时,就会按照float类型的地址进行解析,所以会出现错误
int main()
{
int a = 0;
char* p1 = &a;
short*p2 = &a;
int* p3 = &a;
printf("%p",p1);
printf("%p",p2);
printf("%p",p3); //这三个打印的都一样
}
3.指针类型
1.正常指针
2.野指针 :定义之后没有赋值
3.空指针 :定义后初始化为NULL
4.失效指针 ;调用变量时还处于它的生存期时,则没有失效
int * p()
{
int a =100;
int * p = &a;
return p;
}
int main()
{
int *k = p();
printf("%d",*k);//ERROR,失效指针,a定义在另外一个函数栈帧中,虽然函数结束后,a变量中存储的值没有被清零,但是这块空间的使用权已经交给操作系统了。
int a = 10;
int *p =NULL;
if(a==10)
{
int x =20 ;
p=&x;
}
printf("%d",*p);//x的范围之中if控制的代码块中,此时也是失效指针
}
一道题
void fun(int*p)
{
static int b = 20;
*p = 2;
p = &b;
}
int main()
{
int a = 0;
int* s = &a;
fun(s);
printf("%d %d",a,*s);// 2 2 此时p指向的是b的地址,不过p和s没有任何关系,所以*s 的值为2
}
要注意的点
int main()
{
int ar[5] = {12,23,34,45,56};
int * p = ar;
int x=0,y=0;
x = *p++;
y = *p;
printf("%d %d\n",x,y);//12,23
x = ++*p;
y = *p;
printf("%d %d\n",x,y);//24 24 记住*p是p所指向的那个存储单元,而不是里面存储的值
x = *++p;
y = *p;
printf("%d %d\n",x,y);//34 34
}
记住*p是p所指向的那个存储单元,而不是里面存储的值
int main()
{
int arry[5] ={1,2,3,4,5};
int *p1 = &a[0];
int *p2 = &a[4];
int *p3 = &a[5];
printf("%d",(p2-p1));//4 并不是16
printf("%d",(p3-p1));//5 c语言中没有下角标越界的检查
}
要注意的一道题
const char* str[] = { "hello","new","printf","scanf","const","main","static" };
const char* s = str[2];
int n = sizeof(str) / sizeof(str[0]);
for (int i = 0; i < n; i++)
{
printf("%s\n", s);
s++;
}
printf("%d\n", sizeof(str));
printf("%d\n", sizeof(str[2]));
return 0;
结果为printf,rintf,intf,ntf,tf; 28,4
str 为一个存储五个指向数据区的字符串的指针,
3.1.1指针的类型
int * P[10];//一个长度为10的数组,每个元素是指向int*的指针
int (*p)[10];//指向长度为10的数组的指针
int * (*p)[10];//指向长度为10的数组的指针的指针
int * fun(int a,int b);//返回一个整型指针
int (*fun)(int ,int );//一个形参为(int,int),返回值为int的函数指针
int *(*fun)(int,int);//一个形参为(int,int),返回值为int*的函数指针
int (*fun[3])(int,int);//一个长度为3的数组,每个元素为指向形参为(int,int)返回值为int的指针
double **s;
double *p0,p1,p2,p3;
double a0,a1,a2,a3;
1.s+1:p1地址
2.*s+1:a0地址
3.**s+1: a0+1
十一.结构体
程序员自己设计出来的类型
1.注意点
1.不能在定义的时候给它的成员变量赋值,因为它是定义,像一张图纸,并没有开辟空间,所以并不能赋值。
2.结构体变量在定义时,如果给所有成员变量都没有赋值,那么所有成员变量都是随机值,如果只给部分成员变量赋值,那么其他的成员变量的值默认为0
3.结构体变量之间可以互相赋值
struct student
{
int age = 4;//ERROR
char [20] name;
};
int main()
{
struct student s ={4};//如果只给一部分成员变量赋值,那么其它的成员变量默认值为0
struct student b;
b=s;//结构体类型的变量之间可以相互赋值,因为它也是数据类型,只不过是自己设计的
}
2.访问成员变量
- 通过"."
- 通过"->"结构体指针使用的方法 例 sp->age 等同于(*sp).age
int main()
{
struct student
{
int age;
char [20] name;
};//大小 4+20字节
struct student s1 = {4,"小明" };//定义并初始化
struct student sp = &s1;
printf("%d",s1.age);
printf("%s",s1.name);
printf("%d",sp->age);//等于(*sp).age
printf("%s",sp->name);//等于(*sp).name
}
也可以这样
int main()
{
struct sutdent
{
char [] name;
int age;
};
struct student student;//定义了一个名字为student 的struct student类型的变量。(为什么可以起student这个名字,因为它的类型名为struct student)
}
尽量不要用结构体变量做函数的形参,因为结构体变量也是数据类型,它也会在函数的栈帧中开辟空间,并把实参的值传过去,这样太损耗时间和空间。传递的话尽量使用指针。
如何实现数组平移 ?
平移3 int a ={1,2,3,4,5,6,7,8,9,10};
交换前3位,交换后3位 此时 a 为{3,2,1,10,9,8,7,6,5,4};
再整体一交换 此时 a 为{4,5,6,7,8,9,10,1,2,3}
利用结构体
struct Ar_move {
int a[10];
int index;
int maxsize;
};
int Get_Elem(const struct Ar_move *par,int pos)
{
if (par == NULL || pos <0 || pos>par->maxsize )
exit(1);
return par->a[(par->index + pos) % par->maxsize];
}
void Left_move(struct Ar_move* par, int k)
{
if (par == NULL)
exit(1);
k = k % par->maxsize;
par->index = par->index + k;
}
int main()
{
struct Ar_move a = { {1,2,3,4,5,6,7,8,9,10},0,10 };
Left_move(&a, 3);
for (int i = 0; i < 10; i++)
{
printf("%d", Get_Elem(&a, i));
}
}
3.结构体大小,关于字节对齐
3.1.使用预处理指令改变对齐方式
#pargma pack(n) n为1,2,4,8,16
3.2.对齐规则(没有使用预处理指令改变对齐方式)
1.结构体变量的首地址,必须是结构体变量中的“最大基本数据类型成员 所 占字节数”的整数倍。
2.结构体变量中每个成员相对于结构体首地址的偏移量,都是该成员基本数据类型所占字节的整数倍。
3.结构体变量总的大小,为结构体变量中最大基本数据类型的字节的整数倍、
struct a
{
char cha;
int b;
char chc;
}
cha占一个字节,因为下面b为4个字节,此时偏移量为1,所以在下面填充3个字节,此时chc偏移量为9个字节,因为9不能被4整除,所以再在下面填充3个字节,所以此结构体大小为12个2字节
struct sdate
{
int year;
int month;
int day;
};
struct student
{
char s_id[10];
char s_name[8];
struct sdate birthday;
double grade;
};
这种需要把结构体拆开,对齐的时候按Int year,int month,int day 来看,而不是按整个结合体来对齐
3.3计算偏移量
1.
struct Node
{
char chb[3];
int d;
double e;
char chf[3];
int g;
};
int main()
{
struct Node x;
int len = (char*)&x.d - (char *)&x;
printf("%d", len);
}
struct Node
{
char chb[3];
int d;
double e;
char chf[3];
int g;
};
int main()
{
int len = (int)(&((struct Node*)0)->d);
printf("%d", len);
;
}
为什么要字节对齐?
1.内存的大小是字节,但是,计算机CPU读取内存时并非逐字节读取,而是以2,4,8,的字节块来读取因此对数据类型的地址做出限制
2.有些平台每次读都是从偶地址开始,如果从偶数开始读,则只需要一个周期,如果存在奇地址,则需要两个周期,再对数据进行拼凑
3.不同平台的对齐方式不同,同样的结构在不同的平台大小不同,如果在传输数据的时候,可能会出现混乱
十二.共用体
共用体(联合体)对一块地址以不同的方式解析
1.共用体的每一个数据成员的起始地址都相同,所有成员共用同一段内存,修该一个数据成员会影响其他成员的值
2.共用体所占内存大小,等于最大成员所占大小,共用体采用内存覆盖技术,同一时刻只能保存一个成员的值,对新的成员赋值,则会更改原来成员的值
union a
{
int a;
char s[4];
};
union b
{
int a;
char s1, s2, s3, s4;
};
union c
{
int a;
struct {
char s1, s2, s3, s4;
};
};
利用共用体证明数据的存放是小端地址
union a
{
short b;
char c[2];
};
int main()
{
union a a1;
a1.short = 0x0001;
if(a1.c[0] == 1)
{
printf("为小端存放模式");
}else
{
printf("为大端存放模式");
}
}
利用共用体转化IP地址
union ip
{
unsigned int addr;
struct
{
unsigned char s3,s2,s1,s0;
};
};
void print_IP(int addr,char * buff)
{
union ip a= { addr };
printf("%d.%d.%d.%d\n", a.s0, a.s1, a.s2, a.s3);
sprintf(buff, "%d.%d.%d.%d", a.s0, a.s1, a.s2, a.s3);
}
int main()
{
char buff[20] = { 0 };
print_IP(2148205343,buff);
printf("%s", buff);
;
}
printf底层调用了sprintf,printf是把数据格式化之后打印到屏幕上,而ssprintf是把数据格式化之后送到指定的区域
如:printf(“%d %d\n”,1,2);
转化之后为
1 2\n\0,然后把它显示在屏幕上
共用体有名字的时候是一个声名,不占内存
十三.动态内存
1.分配方式
分配内存的时候有两种方式
占用块:一般在低地址区,给用户分配的部分
空闲块:一般在高地址区,未给用户分配的部分
1.1.用户请求分配时,直接在高地址去分配,不理会分配给用户的空间是否空闲,直至空闲块无法分配,系统才去回收那些不再使用的空闲块(free之后的占用块),并重新组织内存,将所有空闲区域连接在一个成为一个大的空闲块
1.2.用户请求分配,系统先去找有没有合适的用户不使用的空闲块(free后的占用块),如果没有再去空闲块分配
vs为第二种
栈的默认大小为1M,如果你使用的是VS的话,可以在这里修改栈的大小
尽管在C99中规定了数组可以动态开辟,就像这样
scanf(“%d”,&n);
int arr[n];
但是大多数编译器还没有实现,因为栈的空间固定是1M,如果像这样让用户直接输入,如果输入的值大于1M,这样会之间冲毁栈帧。
2.如何动态分配内存
常用函数 malloc calloc,realloc,free,需要引用stdlib.h或者malloc.h
2.1.void * malloc(size_t size ) size为字节数
fd为上越界标志,它为所有开辟的空间默认初始为cd
如果malloc的字节为0?它还会不会分配空间?
int main() {
int* t = (int*)malloc(0);
}
会分配,这样的话返回的地址就为两个越界标志的地址
因为这样也指的是一块地址,所以它也可以对这地址进行操作,不过这样特别危险
2.2.void * calloc(size_t num,size_t,size)
分配好之后,每个空间初始为0
底层调用了malloc,相当于这样的
void* calloc(size_t num, size_t size) {
void* t = (num * size);
if (t == NULL)
exit(1);
if (t != NULL) {
memset(t, 0, num * size);
}
return t;
}
**3.3 void recalloc(void ptr,size_t new_size)
重新分配内存块,当传入的地址为NULL时,和mallc基本一样
int main()
{
int* p = (int*)malloc(sizeof(10));
if (p == NULL)
exit(1);
int* newp = (int*)malloc(sizeof(15));
if (newp == NULL)
exit(1);
p = newp;
}
尽量新定义一个指针去开辟,因为如果分配失败recalloc返回值为NULL,这样会把原来的地址弄不见
int main(){
int* p = (int*)malloc(sizeof(10));
if (p == NULL)
exit(1);
p=(int *)malloc(5);
}
最好不要使用这个减容,因为这样会造成内存碎片
小心野指针的使用,释放之后一定要把指针置为空
int main() {
int* t = (int*)malloc(10);
free(t);
int* s = (int*)malloc(10);
*t = 100;
printf("%d", *t);
free(t);
return 0;
}
t释放之后并没有置空,所以它还指向这块空间,而vs采取的是上面我说的第二种方式,所以s也指向的是t之前的那块空间,这样的话,t可以修改s指向的那块空间,也可以释放,这样是非常危险的,所以一定要养成好习惯,指针一释放立马置为NULL
malloc ,calloc,realloc这三类分配的空间的结构
头部信息 28字节
上越界
空间
下越界
头部信息可以分为三部分:
1.第一部分,标志信息,用于标记这块空间是否释放
2.第二部分,系统把所有分配的空间都相当于用一个链表连接起来(便于在程序结束后释放空间),这部分存储上一块分配空间的地址还有下一块分配空间的地址
3.第三部分,存储了分配这块空间的大小。
分配的内存可以释放一半吗?
int main(){
int * p=(int *)malloc(sizeof(10));
p+=5;
free(p);
return 0;
}
所以这样的操作是不行的,空间释放的时候,先向上偏移到头部信息,再根据头部信息来释放空间,而且空间的分配都是整块整块分配的,绝对不会存在这种释放一半的情况。
十四.文件
1.四种流
stdin 标准输入文件 一般指键盘 scanf getchar等默认从stdin获取输入
stdout 标准输出文件 一般指显示屏 printf putchar 等默认向stdout 输出
stderr 标准错误文件 一般指显示器 perror等默认向stderr输出
stdprn 标准打印文件 一般指打印机
默认下 stdin是行缓冲,它默认存放在一个buffer中,当输入数据时,数据会一个个存放在缓冲区,只有当输入\n时,才会一个一个存缓冲区取数据
默认下 stdout是行缓冲,它默认存放在一个buffer中,只有当换行时,才会输出到屏幕
stderr默认无缓冲会直接输出
1. 打开文件函数 原型FILE * fopen(const char * filename,const char * mode)
filename,文件名,包含路径
mode,打开模式
“r”,只读 文件必须存在,否则打开失败
“w”,只写 若文件存在,则清除文件重新写入,否则,则新创建文件
“a”,末尾只写 若文件存在,则将位置指针移到文件末尾,在尾部追加数据,否则,则新创建文件
“r+”,读写 文件必须存在
“w+”,读写 新创建文件
“a+”,读写 在"a"的基础上,新填读功能
“rb”,二进制只读 以二进制形式读
"wb"二进制只写 以二进制形式写
"ab"二进制末尾只写
"rb+“二进制文件读写 功能模式像"r+”,只不过以二进制形式
"wb+“二进制文件读写 功能模式像"w+”,只不过以二进制形式
"ab+“二进制文件读写 功能模式像"a+”,只不过以二进制形式
如果打开失败,则会返回一个NULL
当对文件读取时分配的缓冲区
实际对磁盘的读写是通过文件流对磁盘进行读写,当我们执行对文件写的操作时,实际把数据写入到fp对应的缓冲区(大小为4k, 因为每一页或者每个数据块的大小都是4K),当执行fopen之后,它会把数据回写到磁盘
流按方向分:
输入流: 从文件获取数据称为输入流,
输出流 : 向文件输出数据称为输出流
按数据形式分:
文本流: 文本流是ASCALL序列,比如把123写入文件中,则写入的是对应的字符123,_itoa和这个很像
二进制流 : 二进制流是字节序列
简单用法
int main() {
int arr[5];
int n = sizeof(arr) / sizeof(arr[0]);
FILE* file = fopen("text.txt", "w");
if (file == NULL) {
exit(1);
}
for(int i =0;i<n;i++)
fscanf(stdin, "%d", &arr[i]);
for (int i = 0; i < n; i++) {
fprintf(file, "%d ", arr[i]);
fprintf(stdout, "%d ", arr[i]);
}
fclose(file);
file = NULL;
int arr2[5];
FILE* rfile = fopen("text.txt","r");
if (rfile == NULL) {
exit(1);
}
for (int i = 0; i < n; i++) {
fscanf(rfile, "%d", &arr2[i]);
printf("%d", arr2[i]);
}
}
二进制流基本用法
int main() {
int arr[] = { 1,2,3,4,5 };
int n = sizeof(arr) / sizeof(arr[0]);
FILE* file = fopen("t.txt", "wb");
if (file == NULL) {
exit(1);
}
fwrite(arr, sizeof(arr[0]),n, file);
fclose(file);
file = NULL;
int arr1[5];
FILE* rfile = fopen("t.txt", "rb");
if (rfile == NULL) {
exit(1);
}
fread(arr1, 4, 5, rfile);
fclose(rfile);
rfile = NULL;
printf("%d", arr1[1]);
}
gets_s,读取的时候还要有一个空间来读取\0
2.文件位置函数
2.1 :feof 检测是否到达文件末尾,如果到到达文件末尾,则为非0值,否则则为0值
2.2:rewind 将文件内部指针指向文件的开头(通常用这个清除stdin的缓冲区)
2.3: ftell 返回当前文件位置指示值
2.4:fseek 将文件位置指示符移动到文件中指示的位置
2.5: long ftell(FILE *stream)
2.6: int fseek(FILE *stream ,long offset,int origin)
第一个参数stream为文件指针
第二个参数offset为偏移量,正数表示向右偏移,负数表示向左偏移
第三个参数origin设定从文件的哪里开始偏移,可取值为:
SEEK_CUR(当前位置),SEEK_END(文件末尾),SEEK_SET(文件起始)
简单用法
int main() {
int value = 0, pos = 0;
FILE* file = fopen("info", "rb");
while (scanf("%d", &pos), pos != -1) {
fseek(file, pos * 4, SEEK_SET);
printf("%d\n", ftell(file));
fread(&value, sizeof(int), 1, file);
printf("%d\n", value);
printf("---");
}
fclose(file);
file = NULL;
}
读文件的一个问题 注意!!!
当文件以文本形式打开时,原来文件中的 0D(\r) 0A(\n),只会把0A读入到缓冲区****而使用ftell计算大小时,却会把0D计算上,所以尽量使用二进制读取文件,
为什么会出现这个原因哪,在计算机发明前,对于打字机来说:\r: 把位置移到这行开头 ,\n:把位置移到下行这个位置,而计算机发明之后,就用\n代替了这两个功能
读的时候可以这样读
int main() {
FILE* file = NULL;
errno_t x = fopen_s(&file, "源.c", "rb");
int number=0;
if (file == NULL) {
exit(1);
}
fseek(file,0,SEEK_END);
number = ftell(file);
char* buff = (char*)malloc(sizeof(char) * number);
rewind(file);
fread(buff,sizeof(char),number, file);
printf("%s", buff);
fclose(file);
file = NULL;
}
带缓冲区的原因
当cpu遇到stdin的函数时,先去干其他的事情,当输入\n时,在过来把缓冲区的数据取出来。为了使两个运行速度差别很大的设备可以匹配起来,加快程序的运行(键盘输入的速度与cpu执行速度相差太大)
十五.void 类型
如果给参数列表为void的函数传入参数会怎么样?
void fun1(int a)
{
int* p = &a;
printf("%d \n",*p);//12
p++;
printf("%d \n",*p);//23
p++;
printf("%d \n",*p);//45
}
int main()
{
fun1(12,23,45);
return 0;
}
上面fun1打印的结果依次为12 13 45
形参为void只是给程序员看的,尽管函数形参设置为void但是它还是会接受值(实参入栈是由右向左)如果传入过多的话,会把函数的栈帧冲掉
1无类型指针可以指向任意地址,
void 不能定义变量,但可以定义指针,它可以指向任意变量的地址,包括自己的地址,所有它也可以被称为泛型指针
泛型指针的使用
int main()
{
int a;
double b;
int arry[5];
void* p = &a;
p = &b;
p = arry;
p = &arry;
p = &p;
char* c = (char *)p;//把泛型指针给其它指针赋值时,必须强制类型转化
}
2.对于标准c 和 GUN,无类型指针都不能解析,
3.无类型指针可以sizeof计算这个指针的大小,,而不能计算他所指向的大小
int main()
{
int a;
void* p =&a;
int size1 = sizeof(p);//ture
int size2 = sizeof(*p);//ERROR
}
4.对于标准c:无类型指针无法进行加减操作,如 p++,p–;
对于GUN,它认为void* 和 char *一样,所有可以p++;
初始化函数
void my_memset(void* dest,unsigned char val,size_t count)
{
assert(dest!=NULL);
char* cp =(char*)dest;
while(count--)
{
*cp = val;
cp++;
}
}
int main()
{
int ar[5];
my_memset(ar,0,sizeof(5));
for(int i=0;i<5;i++)
{
printf("%x",ar[i]);//0x0a0a0a0a
}
}
拷贝函数
void* my_memcpy(void* dest,const void* src,size_t,count )
{
if(dest == NULL || src ==NULL)
return dest;
char* dp = dest;
const char* sp =src;
while(count--)
{
*dp = *sp;
dp++;
sp++;
}
return dest;
}
十六.预处理
c语言提供多种预处理功能,如宏定义(#define),文件包括(#include),条件编译(#ifdef),合理使用预处理编写的程序便于 阅读,修改,移植,调试
1.预处理要注意的点
1.1.所有预处理指令必须以#开头,而且预处理指令必须单独占一行
1.#define macro__name char_sequence
#define定义了一个宏名标识符和一个字符序列,在源程序中每次遇到这个宏名(一般使用大写),就用这个字符序列进行替换,过程称为宏替换
gcc编译器可以通过命令行给宏定义符号
在标识符和字符序列之间可以有任意个空格
1.2.宏名可以嵌套定义
#define ONE 1
#deinfe TWO ONE+ONE
1.3.注意宏替换会把后面一行所有的字符序列全部替换(在不使用续航符的情况下)
下面是这个.c文件生成的.i文件
使用续航符,续航符后不允许跟任何符号,空格也不可以
这让我想起来了字符串的一种这样写的方式
当然也可以这样
这样也可以输出
.2.预处理的变元
这样和函数很像的用法我们称为宏函数,它提高了代码的执行速度,减少了调用函数的开销,不过会造成代码的膨胀
每个变元都要加括号,防止展开时于其他运算符进行结合
也可以有多个变元
3.预处理器运算符
1.# 通常称为字符串化运算符,使得它后面的变元变成带双引号的串
2.## 通常称为链接运算符,用于链接两个符号
3.#@ 将后面的变元字符化
如果同时把多个数字字符串化
1 2 3 4
31 32 33 34( 对应的ASCALL值的16进制)
然后把最低位赋值给a
4.宏展开的过程
1.先执行字符串化操作
2.再执行链接操作
3.再对变元进行替换
4.对展开的结果查看是否有#define定义的符号,如果有,则重复上一过程,但不允许对宏名二次展开
#define INC(x) x+1
#define STR(b) #b
int main() {
STR(INC(2));
}
#define INC(x) x+1
#define STR(b) b
int main() {
STR(INC(2));
}
第一个把STR展开后变成了:“INC(2)”,虽然它于宏名相同,但是因为它已经是字符串了所以不再展开
4.2数据类型也可以是变元
5.泛型编程
6.自动生成代码
和这样很像
7.预定义宏 已经定义好的宏
1_FILE_ 展开问当前文件名,为字符常量
2._LINE_展开为源文件行数,为整形常量
3._DATE_展开为日期,格式为"mm dd yyyy"的字符常量
4._TIME_展开为编译时间,格式为"hh:mm:ss"的字符串常量
5._STDC_判断当前编译器是否为标准c编译器,如果是,则展开成1
6._cplusplus 如果以c++方式编译,则该宏存在
7. func 打印当前函数的名称 它不是宏,而是一个静态数组,在编译的时候,编译器会把当前函数的名字放在这个数组中
8.条件编译指令
#if
#else
#elif
#endif
条件后面只能为 宏定义的变量,而且只能为整形.
条件编译是在预处理的时候就处理的,因为如果是局部变量或者全局变量,静态变量,在预处理的时候都默认为0,所以只能为宏定义的变量
#ifdef
#ifndef
这两个也算条件编译,只不过只是检测是否宏定义