提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
文章目录
- 前言
- 一. 数组
- 二.操作符
- 三.指针
- 1.指针的类型
- 2.指针所指向的类型
- 3.指针的值----或者叫指针所指向的内存区或地址
- 4.指针本身所占据的内存区
- 5.指针类型的意义
- 6.野指针
- 7.指针运算
- 8.指针练习题
- 8.1作业练习题
- 8.1.1练习题1:两个int(32位)整数m和n的二进制表达中,有多少个位(bit)不同?
- 8.1.2练习题2:获取一个整数二进制序列中所有的偶数位和奇数位,分别打印出二进制序列
- 8.1.3练习题3:统计二进制中1的个数
- 8.1.4练习题4:指向short类型的指针解引用的权限、步长
- 8.1.5练习题5:指向char类型的指针解引用的权限、步长
- 8.1.6练习题6:下面代码输出的结果是(难!!!)
- 8.1.7练习题7:求Sn=a+aa+aaa+aaaa+aaaaa的前5项之和,其中a是一个0到9的整数
- 8.1.8练习题8:写一个函数打印arr数组的内容,不使用数组下标,使用指针。
- 8.1.9练习题9:求出0~100000之间的所有“水仙花数”并输出。
- 9.字符指针
- 10.指针数组
- 11.数组指针
- 12.数组传参和指针传参
- 13.函数指针
- 14.函数指针数组
- 15.指向函数指针数组的指针
- 16.指针和数组面试题的解析
- 四.结构体
- 五.实用调试技巧
- 六.字符和字符串的库函数
- 七.数据的存储
- 八.运算符重载汇总
前言
本文目的是记录自学过程中碰到的概念不清的问题,一些基础知识的简介会忽略
推荐几篇详解基础知识的文章:
C++编程入门知识总结 | 内附完整的源代码示例
13 万字 C 语言从入门到精通保姆级教程2021 年版
欢迎各位大佬纠错提议。
一. 数组
1.数组名
数组名一般代表首元素地址,有两种情况例外。
①sizeof(arr)
②&arr
sizeof(arr):统计整个数组在内存中的长度;
&arr:取整个数组的地址,虽然跟首元素地址是一样的,但通过程序测试会发现 &arr+1在原地址基础上加的是整个数组空间大小
arr:首元素地址,arr+1第二个元素地址
int main()
{
int arr[10]={0};
cout<<&arr<<endl;
cout<<&arr+1<<endl;
cout<<arr<<endl;
cout<<arr+1<<endl;
return 0;
}
2.数组传参
void test1(int arr[])
{
cout<<sizeof(arr)<<endl;//arr传入函数是首元素地址,地址大小看操作系统,32位的是4字节,64位的是8字节
}
void test2(char ch[])
{
cout<<sizeof(ch)<<endl;//同理,ch传入函数是首元素地址,地址大小看操作系统,32位的是4字节,64位的是8字节
}
int main()
{
int arr[10]={0};
char ch[10]={0};
cout<<sizeof(arr)<<endl;//40
cout<<sizeof(ch)<<endl;//10
test1(arr);
test2(ch);
return 0;
}
二.操作符
sizeof():操作符,计算变量/类型所占内存大小(字符串的话包括\0),单位是字节
strlen():函数,求字符串长度的,找\0之前出现的字符个数
size_t __cdecl strlen (const char * str)
{
const char *eos = str;
while( *eos++ ) ;
return( eos - str - 1 );
}
C类型 | 32位 | 64位 |
---|---|---|
char | 1 | 1 |
short int | 2 | 2 |
int | 4 | 4 |
long int | 4 | 8 |
long long int | 8 | 8 |
(char*等)地址 | 4 | 8 |
float | 4 | 4 |
double | 8 | 8 |
1.表达式的值
表达式有以下几种:常量表达式,关系表达式,运算表达式,逻辑表达式,赋值表达式,逗号表达式……
int main()
{
int a=3,b=5;
cout<<"4:"<<4<<endl; // 常量表达式的值是其本身
cout<<"a>b:"<<(a>b)<<endl;// 关系表达式的值是关系比较的结果(true/false,1/0)
cout<<"a+b:"<<(a+b)<<endl;// 运算表达式的值是运算结果
cout<<"a&b:"<<(a&b)<<endl;//位运算表达式的值是运算结果
cout<<"a&&b:"<<(a&&b)<<endl;// 逻辑表达式的值是逻辑运算结果
cout<<"a=4:"<<(a=4)<<endl;// 赋值表达式的值是=右边表达式的值
cout<<"(a++,9):"<<(a++,9)<<endl;// 逗号表达式的值是最后一个表达式的值
//混合练习
int c=a+b; // 到目前为止a=5,b=5
cout<<"c=a+b:"<<(c=a+b)<<endl; //赋值表达式看右边表达式的值
c=(a+b),b+1;
cout<<"c=(a+b),b+1:"<<(c=(a+b),b+1)<<endl; //赋值表达式看右边表达式的值,逗号表达式的值是最后一个表达式的值,即b+1的值
c=(a+b,b+1);
cout<<"c=(a+b,b+1)"<<(c=(a+b,b+1))<<endl; //赋值表达式看右边表达式的值,逗号表达式的值是最后一个表达式的值,即b+1的值
return 0 ;
}
int main()
{
int i = 0, a = 0, b = 2, c = 3, d = 4;
i = a++ && ++b && d++;
cout<<"a:"<<a<<endl;
cout<<"b:"<<b<<endl;
cout<<"c:"<<c<<endl;
cout<<"d:"<<d<<endl;
cout<<"i:"<<i<<endl;
return 0 ;
}
i = a++ && ++b && d++;
先算a++这个表达式的值为a,就是0,0&&任意数结果都是0,所以程序直接跳过后面的运算,++b和d++都会跳过,i直接被赋值0
int main()
{
int i = 0, a = 1, b = 2, c = 3, d = 4;
i = a++ && ++b && d++;
cout<<"a:"<<a<<endl;
cout<<"b:"<<b<<endl;
cout<<"c:"<<c<<endl;
cout<<"d:"<<d<<endl;
cout<<"i:"<<i<<endl;
return 0 ;
}
a++ && ++b && d++:1&&3&&4结果为1
2.整型提升
概念:
C语言中字节数少于整型字节数的数据类型在进行整型运算时,该类型的数据会被默认转为整型数据。
其中,该类型的数据被转化为整型数据的过程就称为整型提升。
实际上就是表达式中的char和short类型数据在进行整型运算时会发生整形提升,变为普通整型。
为什么会发生整型提升呢?
这是由计算机体系结构决定的,我们都知道计算机中的计算都由CPU完成,具体来说是由CPU中的运算器(ALU)完成的。而ALU一般在被设计时,其操作对象——操作数的字节大小被要求至少是int的字节数。此时,我们还要明晰一个事情,那就是数据在被运算时,数据并不是直接在ALU上存储的,而是存储在CPU的寄存器(register)中,而通用寄存器的字节大小与ALU操作数的字节大小保持一致。
整型提升规则:整型提升是按照变量的类型来进行提升的
- 如果补码是无符号数,则高位直接补0;
- 如果补码是有符号数,则高位全补符号位。
练习一下:
int main()
{
char a = 3;
//00000000000000000000000000000011
//00000011 - a
char b = 127;
//00000000000000000000000001111111
//01111111 - b
char c = a + b;
//00000011 //3
//01111111 //127
//10000010 //130
//10000010 - c
//11111111111111111111111110000010 - 补码
//11111111111111111111111110000001 - 反码
//10000000000000000000000001111110 - 原码
//-126
//发现a和b都是char类型的,都没有达到一个int的大小
//这里就会发生整形提升
cout<<(int)c<<endl;; //-126
return 0;
}
如果cout<<c<<endl;c是10000010,即输出ASCII码值为130的字符,这个字符不存在,所以屏幕打印出来的看不到。
int main()
{
char a = 0xb6;//十六进制b6=二进制1011 0110
short b = 0xb600;//十六进制b600=二进制10110110 00000000
int c = 0xb6000000;//十六进制b6000000=二进制//10110110 00000000 00000000 00000000
if (a == 0xb6)
cout<<"a"<<endl;
if (b == 0xb600)
cout<<"b"<<endl;
if (c == 0xb6000000)
cout<<"c"<<endl;
return 0;
}
//0xb6=00000000 00000000 00000000 1011 0110
//char a实际只能接收到后8位,是1011 0110,所以(a == 0xb6) false
//0xb600=00000000 00000000 10110110 00000000
//short a实际只能接收到后16位,是10110110 00000000,所以if (b == 0xb600) false
//0xb6000000=10110110 00000000 00000000 00000000
//int c实际接收到是10110110 00000000 00000000 00000000,是这个32位的数,if (c == 0xb6000000) true
三.指针
指针是一个特殊的变量,它里面存储的数值被解释成为内存里的一个地址。要搞清一个指针需要搞清指针的四方面的内容:指针的类型、指针所指向的类型、指针的值或者叫指针所指向的内存区、指针本身所占据的内存区。
举个栗子:
int*ptr;
char*ptr;
int**ptr;
int(*ptr)[3];
int*(*ptr)[4];
1.指针的类型
从语法的角度看,你只要把指针声明语句里的指针名字去掉,剩下的部分就是这个指针的类型。这是指针本身所具有的类型。
各个指针的类型:
int*ptr;//指针的类型是int*
char*ptr;//指针的类型是char*
int**ptr;//指针的类型是int**
int(*ptr)[3];//指针的类型是int(*)[3]
int*(*ptr)[4];//指针的类型是int*(*)[4]
2.指针所指向的类型
当你通过指针来访问指针所指向的内存区时,指针所指向的类型决定了编译器将把那片内存区里的内容当做什么来看待。
从语法上看,你只须把指针声明语句中的指针名字和名字左边的指针声明符*去掉,剩下的就是指针所指向的类型。
例如:
int*ptr; //指针所指向的类型是int
char*ptr; //指针所指向的的类型是char
int**ptr; //指针所指向的的类型是int*
int(*ptr)[3]; //指针所指向的的类型是int()[3]
int*(*ptr)[4]; //指针所指向的的类型是int*()[4]
3.指针的值----或者叫指针所指向的内存区或地址
概念:指针的值是指针本身存储的数值,这个值将被编译器当作一个地址。在32 位程序里,所有类型的指针的值都是一个32 位整数,因为32 位程序里内存地址全都是32 位长。指针所指向的内存区就是从指针的值所代表的那个内存地址开始,长度为sizeof(指针所指向的类型)的一片内存区。以后,我们说一个指针的值是XX,就相当于说该指针指向了以XX 为首地址的一片内存区域;我们说一个指针指向了某块内存区域,就相当于说该指针的值是这块内存区域的首地址。指针所指向的内存区和指针所指向的类型是两个完全不同的概念。在上述例子中,指针所指向的类型已经有了,但由于指针还未初始化,所以它所指向的内存区是不存在的,或者说是无意义的。
遇到指针应该思考:这个指针的类型是什么?指针指的类型是什么?该指针指向了哪里?
4.指针本身所占据的内存区
指针本身占了多大的内存?你只要用函数sizeof(指针的类型)测一下就知道了。在32 位平台里,指针本身占据了4 个字节的长度;64位平台占8个字节长度。指针本身占据的内存这个概念在判断一个指针表达式是否是左值时很有用。
5.指针类型的意义
1.指针类型决定了:指针解引用的权限有多大(整型指针解引用访问4个字节,字符型指针解引用只能访问1个字节)
2.指针类型决定了:指针走一步,能走多远(步长)
int arr[10] = { 0 };
int *p = arr;
char *pc = arr;
printf("%p\n", p);
printf("%p\n", p+1);//加了4,因为int4字节
printf("%p\n", pc);
printf("%p\n", pc+1);//加了1,因为char1字节
注意:有些平台会出现这样的error:"int*"类型不能用于初始化"char*"类型的实体。
int a = 0x11223344;
char* pc = &a;
*pc = 0;
int* pa = &a;
*pa = 0;//只改变一个字节
*pa=0之后,a变成0x11223300,只改变了一个字节
6.野指针
概念:野指针就是指针指向的位置是不可知的。
6.1指针定义时不进行初始化的话,默认是随机值
例如下例的P就是野指针:
int* p;//p是一个局部的指针变量,局部变量不初始化的话,默认是随机值
*p = 20;//非法访问内存了
养成初始化习惯
6.2指针越界也会造成野指针
如下例:
//越界访问
int arr[10] = { 0 };
int* p = arr;
int i = 0;
for (i = 0; i <= 10; i++)//i=10时,p越界访问;此处改为i<10即可
{
*p = i;
p++;
}
6.3指针指向的空间释放问题造成野指针
int* test()
{
int a = 10;
return &a;
}
int main()
{
int*p = test();
*p = 20;
return 0;
}
a的地址返回给p,a的生命周期就结束了,a的空间还给操作系统了,p的空间还存着0x0012ff40这个地址,如果还想通过这个地址访问a的空间将其改为20,这就非法访问内存了。
6.4指针使用之前检查有效性
int main()
{
//当前不知道p应该初始化为什么地址的时候,直接初始化为NULL
//int* p = NULL;
//明确知道初始化的值
//int a = 10;
//int* ptr = &a;
//C语言本身是不会检查数据的越界行为的
int* p = NULL;
if(p != NULL)
*p = 10;
return 0;
}
if(p!=NULL)再操作指针p
总结:如何规避野指针?
1.指针初始化
2.小心指针越界
3.指针指向空间释放及时置NULL
4.指针使用之前检查有效性(if(p!=NULL)再操作指针p)
7.指针运算
指针±整数
指针-指针
指针的关系运算
7.1指针±整数
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
int* p = arr;
int* pend = arr + 9;
while (p<=pend)
{
cout<<*p<<endl;
p++;
}
return 0;
}
7.2指针-指针
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
char c[5];
cout<<&arr[9] - &c[0]<<endl;//err,不对的哦,两个指针指向不同空间
cout<<&arr[9] - &arr[0]<<endl;//这个输出9
return 0;
}
指针-指针得到的是俩个地址之间元素个数,两个指针指向同一块空间才能相减。
指针-指针的应用:自创函数求字符串长度
int my_strlen(char* str)
{
char* start = str;
while (*str != '\0')
{
str++;
}
return str - start;
}
int main()
{
//strlen(); - 求字符串长度
//递归
int len = my_strlen("abc");//这里"abc"传入的只有首字母a的地址
cout<<len<<endl;
return 0;
}
7.3指针的关系运算
C语言标准规定,允许指向数组元素的指针与指向数组最后一个元素后面的那个内存位置的指针比较,但是不允许与指向第一个元素之前的那个内存位置的指针进行比较。
7.4数组和指针
arr[i] <=> i[arr]<=> *(arr+i) <=> *(i+arr) <=> *(p+i) <=> *(i+p)
上述表达式完全一样
[] 是一个操作符 i和arr是两个操作数
arr[i]<=>i[arr]
8.指针练习题
8.1作业练习题
8.1.1练习题1:两个int(32位)整数m和n的二进制表达中,有多少个位(bit)不同?
输入例子:1888 1999
int main()
{
int m = 1888; //0000 0000 0000 0000 0000 0111 0110 0000
int n = 1999; //0000 0000 0000 0000 0000 0111 1100 1111
int tmp = m ^ n;//^0000 0000 0000 0000 0000 0000 1010 1111
int count = 0;
while (tmp)
{
tmp = tmp & (tmp - 1);//a=a&(a-1)作用是去掉a二进制最低位的1
count++;
}
cout<<count<<endl;
return 0;
}
记住:a=a&(a-1)作用是去掉a二进制最低位的1
8.1.2练习题2:获取一个整数二进制序列中所有的偶数位和奇数位,分别打印出二进制序列
/*
思路:
1. 提取所有的奇数位,如果该位是1,输出1,是0则输出0
2. 以同样的方式提取偶数位置
检测num中某一位是0还是1的方式:
1. 将num向右移动i-1位
2. 将移完位之后的结果与1按位与,如果:
结果是0,则第i个比特位是0
结果是非0,则第i个比特位是1
*/
int main()
{
int num = 2000;
cout<<"二进制下所有位:";
for (int i = 31; i >=0; i -= 1)
{
cout<<((num >> i) & 1);
}
cout<<endl;
cout<<"二进制下偶数位:";
for (int i = 31; i >= 0; i -= 2)
{
cout<< ((num >> i) & 1);
}
cout<<endl;
cout<<"二进制下奇数位:";
for (int i = 30; i >= 0; i -= 2)
{
cout<<((num >> i) & 1);
}
cout<<endl;
return 0;
}
8.1.3练习题3:统计二进制中1的个数
/*
思路:
一个int类型的数据,对应的二进制一共有32个比特位,可以采用位运算的方式一位一位的检测,具体如下
*/
int main()
{
int num = 1999;//0000 0000 0000 0000 0000 0111 1100 1111
int a = 0;
int count = 0;
for (int i = 32; i >= 1; i -= 1)
{
a = (num >> i) & 1;
if (a == 1)
count++;
}
cout<<"二进制下所有位中1的个数:";//9
cout<<count<<endl;
return 0;
}
8.1.4练习题4:指向short类型的指针解引用的权限、步长
int main()
{
int arr[] = {1,2,3,4,5};
short *p = (short*)arr;
int i = 0;
for(i=0; i<4; i++)
{
*(p+i) = 0;
}
for(i=0; i<5; i++)
{
cout<<arr[i];
}
return 0;
}
运行前arr数组在内存中的存储格式为:
运行后arr数组在内存中的存储格式为:
指针p的类型为short*类型的,因此p每次只能操作两个字节,for循环对数组中内容进行修改时,依次访问的是:
arr[0]的低两个字节,arr[0]的高两个字节,arr[1]的低两个字节,arr[1]的高两个字节。
运行结果为:
8.1.5练习题5:指向char类型的指针解引用的权限、步长
int main()
{
int a = 0x11223344;
char *pc = (char*)&a;
*pc = 0;
cout<< hex <<a<<endl;
return 0;
}
运行前a的内存
运行后a的内存
char类型的指针变量pc指向只能指向字符类型的空间,如果是非char类型的空间,必须要将该空间的地址强转为char类型。
char pc = (char)&a; pc实际指向的是整形变量a的空间,即44,
*pc=0,即将44位置中内容改为0,修改完成之后,a中内容为:0x11223300
8.1.6练习题6:下面代码输出的结果是(难!!!)
int i;
int main()
{
i--;
if (i > sizeof(i))
{
cout<<">"<<endl;
}
else
{
cout<<"<"<<endl;
}
return 0;
}
全局变量,没有给初始值时,编译其会默认将其初始化为0。
i的初始值为0,i–结果-1,i为整形,sizeof(i)求i类型大小是4,按照此分析来看,结果应该输出<,但是sizeof的返回值类型实际为无符号整形,因此编译器会自动将左侧i自动转换为无符号整型的数据,-1对应的无符号整形是一个非常大的数字,超过4,故实际应该输出>
-1的补码为1111 1111 1111 1111 1111 1111 1111 1111转化为无符号整型数据是2^33-1,远大于4
8.1.7练习题7:求Sn=a+aa+aaa+aaaa+aaaaa的前5项之和,其中a是一个0到9的整数
例如:输入6
>/*
思路:
通过观察可以发现,该表达式的第i项中有i个a数字,因此:
假设第i项为temp,则第i+1项为temp*10+a
具体参考以下代码
*/
int main()
{
int a = 0;
int i = 0;
int sum = 0;
int tmp = 0;
int n=0;
cin>>a;
cin>>n;
for (i = 0; i < n; i++)
{
tmp = tmp * 10 + a;
sum += tmp;
}
cout<<sum<<endl;
return 0;
}
8.1.8练习题8:写一个函数打印arr数组的内容,不使用数组下标,使用指针。
void printArr(int*p,int sz )
{
for(int i=0;i<sz;i++)
{
cout<<*p<<endl;
p++;
}
};
int main()
{
int arr[]={1,2,3,4,5,6,7,8,9};
int sz=sizeof(arr)/sizeof(arr[0]);
printArr(arr,sz);
return 0;
}
8.1.9练习题9:求出0~100000之间的所有“水仙花数”并输出。
“水仙花数”是指一个n位数,其各位数字的n次方之和确好等于该数本身,如:153=1 ^ 3 +5 ^ 3+3 ^ 3,则153是一个“水仙花数”。
#include<iostream>
#include<math.h>
using namespace std;
int main()
{
int i=0;
for(i=0;i<=100000;i++)
{
int sum=0;
int n=1;
int t=i;
//确定i是几位数n
while(t/10!=0)
{
n++;
t=t/10;
};
//获得各个位数数值n次方之和
int m=i;
do{
sum= pow(m%10,n)+sum;
m=m/10;
}while(m!=0);
//判断总数是否等于各个位数数值n次方之和
if(i==sum)
cout<<i<<endl;
}
return 0;
}
9.字符指针
9.1例题1
int main()
{
char str1[] = "hello bit.";
char str2[] = "hello bit.";
const char* str3 = "hello bit.";
const char* str4 = "hello bit.";
if (str1 == str2)
printf("str1 and str2 are same\n");
else
printf("str1 and str2 are not same\n");
if (str3 == str4)
printf("str3 and str4 are same\n");
else
printf("str3 and str4 are not same\n");
return 0;
}
=="hello bit."是常量字符串,常量字符串的内容不能被修改,在内存中只有一份。==所以str3跟str4相等。
10.指针数组
指针数组,顾名思义,是一个存放指针的数组。
比如:
int *arr1[10]; 存放整形指针的数组,每个元素都是一个int *指针
char *arr2[10]; 存放字符指针的数组,每个元素都是一个char *指针
其他类型一样类推,double *arr3[10];float *arr4[10];等等
来看看下面的例子加深理解:
int main()
{
char a[] = { 'a','b','c','d','e' };
char b[] = { 'b','c','d','e' ,'f'};
char c[] = { 'c','d','e' ,'f' ,'g' };
//a,b,c首元素的地址为字符数据的地址,所以可以用char*指针来接受
char* arr1[3] = { a,b,c };
for (int i = 0; i < 3; i++)
{
for (int j = 0; j < 5; j++)
{
printf("%c ", *(arr1[i] + j));
}
printf("\n");
}
return 0;
}
11.数组指针
1.数组指针的定义
数组指针:能够指向数组的指针。
类型为:int (*p)[10]; //int可以换成其他类型,这里以int举例。
*先与p结合,表示p是一个指针变量,然后指向一个大小为10个整形的数组。所以p是一个指针,指向一个数组,叫数组指针。
[ ]的优先级要高于 * 号的,所以必须加上()来保证p先和 * 结合。
为了加深对数组指针的理解,我们先来了解一下数组名的问题!!!
2.&数组名VS数组名
&数组名,表示的是数组的地址,而不是数组首元素的地址。
数组名,表示的数组首元素的地址,两个另外:sizeof(arr); &arr 。
先来看看例子加深一下印象吧!!!
int main()
{
int arr[10] = { 0 };
printf("arr的地址:%p\n", arr);
printf("&arr的地址:%p\n", &arr);
printf("arr+1的地址:%p\n", arr+1);
printf("&arr+1的地址:%p", &arr+1);
return 0;
}
通过运行,可以看到,arr和&arr都是表示的第一个元素的地址,但arr+1表示的是第二个元素的地址00B6F714 - 00B6F710=4,而&arr+1表示的是整个数组的地址+1,00B6F738 - 00B6F710=40。
结论:数组的地址+1,跳过整个数组的大小,所以 &arr+1 相对于 &arr 的差值是arr数组所占内存大小。
3.数组指针的使用
我们知道数组指针指向的是数组,那么数组指针中应该存放的是数组的地址。
那么,我们就可以写出下面的代码举例了。
int main()
{
int arr[10] = {1,2,3,4,5,6,7,8,9,0};
int (*p)[10] = &arr;//把数组arr的地址赋值给数组指针变量p
//我们便可以通过数组指针来对arr数组进行访问了
for(int i=0;i<10;i++)
{
printf("%d ",(*p)[i]);
}
return 0; }
当然,输出结果肯定为arr数组的内容
通过上面的例子,我们知道了,数组指针本就是存放数组的指针,那么我们也可以用于二维数组,来表示第一行的地址。
void print_arr(int(*arr)[5], int row, int col)
{
int i = 0;
for (i = 0; i < row; i++)
{
int j = 0;
for (j = 0; j < col; j++)
{
printf("%d ", arr[i][j]);
}
printf("\n");
}
}
int main()
{
int arr[3][5] = { 1,2,3,4,5,6,7,8,9,10 };
print_arr(arr, 3, 5); //将第一行的地址传给数组指针(*arr)[5]
return 0;
}
照样输出二维数组arr的结果
所以,我们这里又学会用数组指针作为参数,来对二维数组进行输出了。但需要注意的是:数组指针存放的是数组的地址!!!
看完指针数组和数组指针,在看这里的代码,进行理解
int arr[5]; //整形数组,存放整形数据,每个元素是int
int *parr1[10];//指针数组,存放整形指针的数组,每个元素都是int*
int (*parr2)[10];//数组指针,存放数组的指针,每个元素都是int
int (*parr3[10])[5];//是一个存放数组指针的数组,有10个数组指针,每个数组指针指向一个数组,数组大小为5
12.数组传参和指针传参
1. 一维数组传参
#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,传参给二级指针,arr2是指针数组,每个元素都是int *,所以一级指针的地址传参给二级指针是正确的
{}
int main()
{
int arr[10] = {0};
int *arr2[20] = {0};
test(arr);
test2(arr2);
}
一维数组传参,函数形参可以是一维数组或者一级指针;
n级指针数组传参,函数形参可以是n级指针数组或者n+1级指针。(一般只考虑n=1)
2. 二维数组传参
void test(int arr[3][5])//ok,相同类型,可以
{}
void test(int arr[][])//no,省略了第二个[]的数字,不知道一行多少元素,错误
{}
void test(int arr[][5])//ok,省略第一个[]数字,保留了第二个[]数字,是可以的
{}
//总结:二维数组传参,函数形参的设计只能省略行数,不能省略列数。
void test(int *arr)//no,二维数组首元素的地址其实是第一行元素的地址,也就是一维数组,可以用数组指针进行保存,
{}
void test(int* arr[5])//no,这是一个指针数组,每个元素是int *,不是int,所以错误
{}
void test(int (*arr)[5])//ok,二维数组首元素地址是第一行的数据,所以就是有五个元素的数组,数组指针就是指向有几个元素的数组的,所以正确
{}
void test(int **arr)//no,一维数组地址跟二级指针不匹配,错误
{}
int main()
{
int arr[3][5] = {0};
test(arr);
}
总结:二维数组传参,函数形参必须是有列数的二维数组或者有列数的一维数组指针。
3. 一级指针传参
//一级指针上面讲了的,这里就是用指针变量p指向一维数组arr首元素的地址
//然后依次对arr数组进行遍历
void print(int *p, int sz) {
int i = 0;
for(i=0; i<sz; i++)
{
printf("%d\n", *(p+i));
}
}
int main()
{
int arr[10] = {1,2,3,4,5,6,7,8,9};
int *p = arr;
int sz = sizeof(arr)/sizeof(arr[0]);
//一级指针p,传给函数
print(p, sz);
return 0; }
4. 二级指针
#include <stdio.h>
void test(int** ptr) {
printf("num = %d\n", **ptr);
}
int main()
{
int n = 10;
int*p = &n;
int **pp = &p;
test(pp);//传二级指针地址
test(&p);//取一级指针地址给二级指针**ptr
//这里有不懂的地方,去看上面的二级指针
return 0; }
void test(char **p) {
}
int main()
{
char c = 'b';
char*pc = &c;
char**ppc = &pc;
char* arr[10];
test(&pc);
test(ppc);
test(arr);
//Ok,这里传的是指针数组arr首元素的地址,由于每个元素都是char *
//所以取一级指针地址是可以传参给二级指针**p的
//二维数组传参也涉及到了的
return 0; }
总之,你实参传的是什么,你形参就要写相同的类型,反过来也就是形参写的什么,实参也要传相同的类型!
13.函数指针
函数指针是为了有时候方便我们调用函数,比如下面涉及到的转移表。
作用:
1、传递参数给另一个函数;
2、使用转移表简化代码;
大家首先来看一段代码
#include <stdio.h>
void test()
{
printf("hehe\n");
}
int main()
{
printf("%p\n", test);//取的函数名,也就是函数地址
printf("%p\n", &test);//取函数地址,是整个函数的地址
//这里需要注意 test==&test;
return 0; }
总结:
&数组名!=数组名
&函数名==函数名
代码中涉及到函数的地址,那我们如何去保存函数的地址,那就需要用到函数指针了,函数指针是用来保存函数地址的。
void test()
{
printf("hehe\n");
}
void (*pfun1)();//函数指针的写法
//*先与pfun1结合,说明pfun1是一个指针,指针指向的是一个函数,指向的函数无参数,返回值类型为void
总结:*返回值类型 (函数指针名)(函数形参1类型,函数形参n类型…)=&函数名;
*例如 int(p)(int,int)=&Add;
我们再来进一步了解下函数指针和函数,先看代码块!!!
int add(int a, int b) {
return a + b; }
int sub(int a, int b) {
return a - b; }
int mul(int a, int b) {
return a*b; }
int div(int a, int b) {
return a / b; }
int main()
{
int (*p1)(int, int) = add;//取add函数地址
int (*p2)(int, int) = sub;//取sub函数地址
int (*p3)(int, int) = mul;//取mul函数地址
int (*p4)(int, int) = div;//取div函数地址
int x = 10, y = 5;
int ret = 0;
//1、原来的调用方法是通过函数名传参进行调用,如下
ret = add(x,y); //这样进行调用
//2、函数指针调用函数方法
ret = (*p1)(x, y);//通过函数指针p1调用add函数
printf("%d\n", ret);
ret = (*p2)(x, y);//通过函数指针p2调用sub函数
printf("%d\n", ret);
ret = (*p3)(x, y);//通过函数指针p3调用mul函数
printf("%d\n", ret);
ret = (*p4)(x, y);//通过函数指针p4调用div函数
printf("%d\n", ret);
return 0;
}
上面代码看完了,再来看看套娃的函数指针,加深理解,下面代码在《C陷阱和缺陷》这本书中涉及到。
//代码1
(*(void (*)())0)();
//调用0地址处的函数,该函数无参,返回类型为void
//1、void (*)() - 函数指针类型
//2、(void (*)())0 - 将0进行强制类型转换,被解释为一个函数的地址
//3、*(void (*)())0) - 对0地址进行了解引用操作
//4、(*(void (*)())0)() - 调用0地址处的函数
//5.也可以写成((void (*)())0)()
//代码2
void (*signal(int , void(*)(int)))(int);
//1、signal先与()结合,说明signal是一个函数名
//2、signal函数第一个参数类型为int,第二个参数类型为函数指针
//该函数指针,指向一个参数为int,返回值为void的函数
//3、signal含糊的返回类型也是一个函数指针
//该函数指针,指向一个参数为int,返回值为void的函数
//signal是一个函数的声明
//代码2的简化
//typedef -对类型进行重定义
typedef void(*pfun)(int);//可以理解为typedef void(*)(int) pfun,但不能这么写;对void(*)(int)这个函数指针类型重命名为pfun;
//对函数指针类型重命名
pfun signal(int,pfun);
总结:函数的返回类型如果是函数指针的话,*号要跟函数名字放在一起。
14.函数指针数组
上面说了函数指针的用处,那么到底是如何用的,先来看看下面的代码块。
函数指针数组写法:void (*p[5])(void x,void y) ,void根据函数类型更改
函数指针数组的作用:用来保存函数的地址
(看转移表)
#include <stdio.h>
int add(int a, int b) {
return a + b; }
int sub(int a, int b) {
return a - b; }
int mul(int a, int b) {
return a * b; }
int div(int a, int b) {
return a / b; }
int main()
{
int x, y;
int input = 1;
int ret = 0;
//将函数的地址保存在函数指针数组中,然后进行调用
int(*p[5])(int x, int y) = { 0, add, sub, mul, div }; //转移表
while (input)
{
printf( "*************************\n" );
printf( " 1:add 2:sub \n" );
printf( " 3:mul 4:div \n" );
printf( "*************************\n" );
printf( "请选择:" );
scanf( "%d", &input);
if ((input <= 4 && input >= 1))
{
printf( "输入操作数:" );
scanf( "%d %d", &x, &y);
ret = (*p[input])(x, y);
}
else
printf( "输入有误\n" );
printf( "ret = %d\n", ret);
}
return 0; }
15.指向函数指针数组的指针
指向函数指针数组的指针,指针指向一个数组,数组的元素都是函数指针
void test(const char* str) {
printf("%s\n", str);
}
int main()
{
//函数指针pfun
void (*pfun)(const char*) = test;
//函数指针数组pfunArr
void (*pfunArr[5])(const char* str);
pfunArr[0] = test;
//指向函数指针数组pfunArr的指针ppfunArr
void (*(*ppfunArr)[10])(const char*) = &pfunArr;
return 0; }
简单来说,就是函数指针数组的指针指向函数指针数组,函数指针数组又指向函数指针,了解即可
16.指针和数组面试题的解析
1.基础题
//一维数组
int a[] = {1,2,3,4};
printf("%d\n",sizeof(a)); //16,数组名单独在sizeof里,计算是整个数组的大小
printf("%d\n",sizeof(a+0));//4,数组名没有单独在sizeof里面,表示首元素地址
printf("%d\n",sizeof(*a));//4,这里为首元素,类型大小为4个字节
printf("%d\n",sizeof(a+1));//4,同第二个
printf("%d\n",sizeof(a[1]));//4,为第二个元素,类型大小为4个字节
printf("%d\n",sizeof(&a));//4,取的整个数组的地址
printf("%d\n",sizeof(*&a));//16,取的是整个数组的地址,然后进行解引用,整个数组大小为16
printf("%d\n",sizeof(&a+1));//4,&a:整个数组地址,+1刚好指向数组的尾部,也是一个地址
printf("%d\n",sizeof(&a[0]));//4,取第一个元素的的地址
printf("%d\n",sizeof(&a[0]+1));//4,取第一个元素的地址,在+1,指向下一个元素的地址
//字符数组
char arr[] = {'a','b','c','d','e','f'};
printf("%d\n", sizeof(arr));//6,整个数组大小
printf("%d\n", sizeof(arr+0));//4,表示第一个元素的地址
printf("%d\n", sizeof(*arr));//1,取第一个元素地址,然后解引用,为第一个元素的字节大小
printf("%d\n", sizeof(arr[1]));//1,第二个元素字节大小
printf("%d\n", sizeof(&arr));//4,整个数组的地址
printf("%d\n", sizeof(&arr+1));//4,第二个元素的地址
printf("%d\n", sizeof(&arr[0]+1));//4,第二个元素的地址
printf("%d\n", strlen(arr));//随机值,不知道在内存何时遇到'\0'
printf("%d\n", strlen(arr+0));//同上
printf("%d\n", strlen(*arr));//错误,strlen库函数原型:strlen(char *str),传参类型不匹配,错误
printf("%d\n", strlen(arr[1]));//同上
printf("%d\n", strlen(&arr));//随机值,上面strlen形参为字符指针,虽然传过去的是整个数组的地址,但strlen只会取第一个元素的地址,所以也是从第一个元素的地址往后数
printf("%d\n", strlen(&arr+1));//随机值-6,跳过整个数组,同上
printf("%d\n", strlen(&arr[0]+1));//随机值-1,从第二个元素开始,同上
//字符串
char arr[] = "abcdef";
printf("%d\n", sizeof(arr));//7,遇到'\0'停止,sizeof会计算'\0'
printf("%d\n", sizeof(arr+0));//4,arr没有单独放在sizeof内,这里为首元素地址
printf("%d\n", sizeof(*arr));//1,数组首元素字节大小
printf("%d\n", sizeof(arr[1]));//1,第二个元素字节大小
printf("%d\n", sizeof(&arr));//4,整个数组的地址
printf("%d\n", sizeof(&arr+1));//4,跳过整个数组,但还是表示地址
printf("%d\n", sizeof(&arr[0]+1));//4,第二个元素的地址
printf("%d\n", strlen(arr));//6,整个数组的长度
printf("%d\n", strlen(arr+0));//同上
printf("%d\n", strlen(*arr));//错误,传参类型不同
printf("%d\n", strlen(arr[1]));//同上
printf("%d\n", strlen(&arr));//6,传参过去,strlen库函数会从首元素地址进行计算,遇到'\0',所以为6
printf("%d\n", strlen(&arr+1));//随机值,跳过字符串,然后从'\0'往后面数,在内存中不知道何时在遇到'\0'
printf("%d\n", strlen(&arr[0]+1));//5,从第二个元素开始数,直到遇到'\0'
char *p = "abcdef";
printf("%d\n", sizeof(p));//4,上面讲到,这里*p其实存放的是首元素的都在
printf("%d\n", sizeof(p+1));//4,第二个元素地址
printf("%d\n", sizeof(*p));//1,首元素字节大小
printf("%d\n", sizeof(p[0]));//同上
printf("%d\n", sizeof(&p));//4,取首元素的地址
printf("%d\n", sizeof(&p+1));//4,第二个元素地址
printf("%d\n", sizeof(&p[0]+1));//4,第二个元素地址
printf("%d\n", strlen(p));//6,从首元素开始数
printf("%d\n", strlen(p+1));//5,从第二个元素开始数
printf("%d\n", strlen(*p));//错误,strlen传参类型不符合
printf("%d\n", strlen(p[0]));//错误,传参不符合
printf("%d\n", strlen(&p));//随机值,在p指针中存的是什么根本不知道,内存好也不知道
printf("%d\n", strlen(&p+1));//随机值,p指针后面存的什么也不知道
printf("%d\n", strlen(&p[0]+1));//5,从第二个元素开始数
//二维数组
int a[3][4] = {0};
printf("%d\n",sizeof(a));//48,整个数组大小,3*4*4=48
printf("%d\n",sizeof(a[0][0]));//4,首元素字节大小
printf("%d\n",sizeof(a[0]));//16,表示的是第一行元素的大小
printf("%d\n",sizeof(a[0]+1));//4,第一行第二个元素的地址,
printf("%d\n",sizeof(*(a[0]+1)));//4,第一行第二个元素字节大小
printf("%d\n",sizeof(a+1));//4,a没有单独放在sizeof内,表示第一行元素地址,+1表示第二行的地址
printf("%d\n",sizeof(*(a+1)));//16,第二行元素大小
printf("%d\n",sizeof(&a[0]+1));//4,&a[0]表示第一行地址,+1表示第二行地址
printf("%d\n",sizeof(*(&a[0]+1)));//16,第二行元素的大小
printf("%d\n",sizeof(*a));//16,a表示第一行元素,所以这里为第一行元素大小
printf("%d\n",sizeof(a[3]));//16,a[3]其实是第四行元素的数组名,所以这里为16;sizeof(表达式),不会计算表达式的值,但能推算它的类型,即使它不存在。
总结: 数组名的意义:
1.sizeof(数组名),这里的数组名表示整个数组,计算的是整个数组的大小。
2.&数组名,这里的数组名表示整个数组,取出的是整个数组的地址。
3.除此之外所有的数组名都表示首元素的地址。
易错点提取:
char arr[] = {'a','b','c','d','e','f'};
printf("%d\n", strlen(arr));//随机值,不知道在内存何时遇到'\0'
printf("%d\n", strlen(arr+0));//随机值
printf("%d\n", strlen(*arr));//err,strlen库函数原型:strlen(char *str),传参类型不匹配,错误
printf("%d\n", strlen(arr[1]));//err
printf("%d\n", strlen(&arr));//随机值,上面strlen形参为字符指针,虽然传过去的是整个数组的地址,但strlen只会取第一个元素的地址,所以也是从第一个元素的地址往后数
printf("%d\n", strlen(&arr+1));//随机值-6,跳过整个数组,同上
printf("%d\n", strlen(&arr[0]+1));//随机值-1,从第二个元素开始,同上
char arr[] = "abcdef";
printf("%d\n", strlen(arr));//6,整个数组的长度
printf("%d\n", strlen(arr+0));//6
printf("%d\n", strlen(*arr));//err,传参类型不同
printf("%d\n", strlen(arr[1]));//err
printf("%d\n", strlen(&arr));//6,传参过去,strlen库函数会从首元素地址进行计算,遇到'\0',所以为6
printf("%d\n", strlen(&arr+1));//随机值,跳过字符串,然后从'\0'往后面数,在内存中不知道何时在遇到'\0'
printf("%d\n", strlen(&arr[0]+1));//5,从第二个元素开始数,直到遇到'\0'
char *p = "abcdef";
printf("%d\n", strlen(p));//6,从首元素开始数
printf("%d\n", strlen(p+1));//5,从第二个元素开始数
printf("%d\n", strlen(*p));//错误,strlen传参类型不符合
printf("%d\n", strlen(p[0]));//错误,传参不符合
printf("%d\n", strlen(&p));//随机值,在p指针中存的是什么根本不知道,内存好也不知道
printf("%d\n", strlen(&p+1));//随机值,p指针后面存的什么也不知道
printf("%d\n", strlen(&p[0]+1));//5,从第二个元素开始数
int a[3][4] = {0};
printf("%d\n",sizeof(a));//48,整个数组大小,3*4*4=48
printf("%d\n",sizeof(a[0][0]));//4,首元素字节大小
printf("%d\n",sizeof(a[0]));//16,表示的是第一行元素的大小
printf("%d\n",sizeof(a[0]+1));//4,第一行第二个元素的地址,
printf("%d\n",sizeof(*(a[0]+1)));//4,第一行第二个元素字节大小
printf("%d\n",sizeof(a+1));//4,a没有单独放在sizeof内,表示第一行元素地址,+1表示第二行的地址
printf("%d\n",sizeof(*(a+1)));//16,第二行元素大小
printf("%d\n",sizeof(&a[0]+1));//4,&a[0]表示第一行地址,+1表示第二行地址
printf("%d\n",sizeof(*(&a[0]+1)));//16,第二行元素的大小
printf("%d\n",sizeof(*a));//16,a表示第一行元素,所以这里为第一行元素大小
printf("%d\n",sizeof(a[3]));//16,a[3]其实是第四行元素的数组名,所以这里为16;sizeof(表达式),不会计算表达式的值,但能推算它的类型,即使它不存在。
2.面试题,重点!!!
笔试题1:
int main()
{
int a[5] = { 1, 2, 3, 4, 5 };
int *ptr = (int *)(&a + 1); //&a表示整个数组地址,+1跳过整个数组,指向数组的尾端
printf( "%d,%d", *(a + 1), *(ptr - 1));
//a表示首元素地址,+1第二个元素地址,(a+1)通过*解引用,结果为第二个元素值
//ptr本指向数组尾端,-1指向第五个元素地址,(ptr-1)通过*解引用,结果为第五个元素值
return 0; }
//程序的结果是什么?
//结果:2 5
笔试题2:
//结构体的大小是20个字节
struct Test
{
int Num;
char *pcName;
short sDate;
char cha[2];
short sBa[4];
}*p;
//假设p 的值为0x100000。 如下表表达式的值分别为多少?
//已知结构体Test类型的变量大小为20个字节
int main()
{
printf("%p\n", p + 0x1);
//0x100014,+1相当于+20
printf("%p\n", (unsigned long)p + 0x1);
//0x100001,强制类型转换为无符号整形,+0x1相当于+整数1
printf("%p\n", (unsigned int*)p + 0x1);
//0x100004,强制类型转换为无符号整形,+0x1相当于+1个整形
return 0; }
笔试题3:
int main()
{
int a[4] = { 1, 2, 3, 4 };
int* ptr1 = (int*)(&a + 1);//ptr1指向数组尾端
int* ptr2 = (int*)((int)a + 1);
//这里假设为小端存储,将a转换为整形,+1就执行第二个字节,ptr2就指向第二个字节
//小端存储不知道的,看我主页博客:数据在内存中的存储
printf("%x,%x", ptr1[-1], *ptr2);
//01 00 00 00 | 02 00 00 00 | 03 00 00 00 | 04 00 00 00
//ptr[-1]相当于*(ptr1-1),所以ptr1指向04的前面,也就是第四个元素
//ptr2这里指向01后面,*ptr2就是 00 00 00 02,但在内存中是0x02 00 00 00,所以输出为2000000
return 0; }
小端存储:低位字节序在低地址,所以00 00 00 02打印出来应该是02000000
笔试题4:
#include <stdio.h>
int main()
{
int a[3][2] = { (0, 1), (2, 3), (4, 5) };
int *p;
p = a[0];
printf( "%d", p[0]);//所以这里为1
return 0; }
注意这里是逗号表达式,取最大值,所以数组a真实值为{1,3,5,0,0,0};
笔试题5:
int main()
{
int a[5][5];
int(*p)[4];
p = a;
printf( "%p,%d\n", &p[4][2] - &a[4][2], &p[4][2] - &a[4][2]);
//结果:FFFFFFFC,-4
return 0; }
笔试题6:
int main()
{
int aa[2][5] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
int *ptr1 = (int *)(&aa + 1);//跳过整个数组,指向数组尾端
int *ptr2 = (int *)(*(aa + 1));//跳过第一行元素,指向第二行元素,然后解引用
printf( "%d,%d", *(ptr1 - 1), *(ptr2 - 1));
//ptr1-1指向最后一个元素10,ptr-1指向第一行的最后一个元素5
//结果为:10,5
return 0; }
笔试题7:
#include <stdio.h>
int main()
{
char *a[] = {"work","at","alibaba"};
char**pa = a;
pa++;
printf("%s\n", *pa);//at
return 0; }
笔试题8:
char *c[] = {"ENTER","NEW","POINT","FIRST"};
char**cp[] = {c+3,c+2,c+1,c};
char***cpp = cp;
printf("%s\n", **++cpp);
printf("%s\n", *--*++cpp+3);
printf("%s\n", *cpp[-2]+3);
printf("%s\n", cpp[-1][-1]+1);
四.结构体
1.结构体声明
结构体:跟数组类似,也是一些值的集合,但是值的类型可以不同,这些值称为成员变量。
特点:
1.结构体是一种构造数据类型
2.把不同类型的数据组合成一个整体
结构的成员可以是标量、数组、指针,甚至是其他结构体
typedef struct Stu
{
char name[20];//名字
int age;//年龄
char id[20];//学号
} s1,s2;//s1和s2也是结构体变量
2.结构体变量的定义、初始化以及结构体成员的访问
//声明 B
struct B
{
char c;
short s;
double d;
};
//声明 Stu
struct Stu
{
//成员变量
struct B sb;
char name[20];//名字
int age;//年龄
char id[20];
} s1,s2;//定义结构体变量s1和s2
//s1,s2是全局变量
int main()
{
//初始化,s是局部变量
struct Stu s = { {'w', 20, 3.14}, "张三", 30, "202005034"};//对象
//. -> 结构体的访问
printf("%c\n", s.sb.c);
printf("%s\n", s.id);
//结构体指针访问指向变量的成员,有时候我们得到的不是一个结构体变量,而是一个指向一个结构体 的指针
struct Stu* ps = &s;//定义指向结构体的指针
printf("%c\n", (*ps).sb.c);
printf("%c\n", ps->sb.c);
return 0;
}
访问形式:<结构体类型变量名>.<成员名> 或者<结构体类型变量名>-><成员名>
注意:结构体变量不能整体引用,只能引用变量成员,如s.sb.c
可以定义指向结构体的指针:定义形式:struct 结构体名 *结构体指针名;
注意:结构体和结构体变量是两个不同的概念:结构体是一种数据类型,是一种创建变量的模板,编译器不会为它分配内存空间,就像 int、float、char 这些关键字本身不占用内存一样;结构体变量才包含实实在在的数据,才需要内存来存储。
3.结构体传参
struct B
{
char c;
short s;
double d;
};
struct Stu
{
//成员变量
struct B sb;
char name[20];//名字
int age;//年龄
char id[20];
};
//结构体传参
void print1(struct Stu t)
{
printf("%c %d %lf %s %d %s\n", t.sb.c, t.sb.s, t.sb.d, t.name, t.age, t.id);
}
//结构体地址传参
void print2(struct Stu* ps)
{
printf("%c %d %lf %s %d %s\n", ps->sb.c, ps->sb.s, ps->sb.d, ps->name, ps->age, ps->id);
}
int main()
{
//s是局部变量
struct Stu s = { {'w', 20, 3.14}, "张三", 30, "202005034" };//对象
//写一个函数打印s的内容
print1(s);//传结构体
print2(&s);//传地址
return 0;
}
结论:结构体传参要传地址。因为函数传参的时候,参数是需要压栈的。如果传递一个结构体对象的时候,结构体过大,参数压栈的系统开销比较大,会导致性能下降。
4.练习题
如有以下代码:
struct student
{
int num;
char name[32];
float score;
}stu;
则下面的叙述不正确的是:( )
A.struct 是结构体类型的关键字
B.struct student 是用户定义的结构体类型
C.num, score 都是结构体成员名
D.stu 是用户定义的结构体类型名
答案解析:
A:正确,在C语言中需要自定义类型时,要用到struct关键字
B:正确:在C语言中,用struct定义的结构体,定义结构体类型变量时,需要用struct student
C:正确:结构体中的变量名称,称之为结构体的成员
D:错误:stu是定义的结构体类型变量,不是名称,如果想要让stu为结构体类型名称时,必须在结构体定义时添加 typedef关键字
五.实用调试技巧
1.调试基本步骤
1.发现程序存在错误
2.以隔离、消除等方式对错误进行定位
3.确定错误产生的原因
4.提出纠正错误的解决方法
5.对程序错误予以改正,重新测试
2.Debug和Release的介绍
Debug通常称为调试版本,它包含调试信息,并且不作任何优化,便于程序员调试程序。
Release称为发布版本,它往往是进行了各种优化,使得程序在代码大小和运行速度上都是最优的,以便用户很好地使用。
我们所说的调试就是在Debug版本的环境中,找代码中潜伏的问题的一个过程。
3.最常用的几个快捷键:
功能 | 快捷键 |
---|---|
代码对齐 | ctrl+k加上ctrl+f |
4.调试时查看程序当前信息
查看调用堆栈
查看汇编信息
查看局部变量
查看寄存器信息
5.经典面试题
5.1以下代码结果为?分析为什么会是这样的结果?(陷阱!!!)
int main()
{
int i=0;
int arr[9]={1,2,3,4,5,6,7,8,9};
for(i=0;i<=11;i++)
{
arr[i]=0;
cout<<"hehe"<<endl;
}
return 0;
}
结果:
输出hehe无限循环
分析:
1.i和arr是局部变量,局部变量是放在栈区上的,栈区内存的使用习惯是:先使用高地址空间,再使用低地址空间;
2.随着下标的增长,数组的地址是由低到高变化的。
3.所以当数组arr越界访问到一定的空间时,就有可能访问/修改i的值。此题中i=11时,arr[i]=0;就把i的内存改为0了,这样在循环里出不去了
六.字符和字符串的库函数
1.strlen
1.1strlen作用与注意点
作用介绍:求字符串长度(不包含\0)
注意:
- 字符串已经 ‘\0’ 作为结束标志,strlen函数返回的是在字符串中 ‘\0’ 前面出-现的字符个数(不包含 ‘\0’ )。
- 参数指向的字符串必须要以 ‘\0’ 结束。
- 注意函数的返回值为size_t,是无符号的( 易错 )
1.2strlen库函数实现:
size_t __cdecl strlen (const char * str)
{
const char *eos = str;
while( *eos++ ) ;
return( eos - str - 1 );
}
例题1(自行判断下输出结果)
#include <string.h>
#include<iostream>
using namespace std;
int main()
{
char* str1 = "abcdef";
char* str2 = "bbb";
cout<<strlen(str1)<<endl;
if (strlen(str2) - strlen(str1) > 0)
{
cout<<"str2>str1"<<endl;
}
else
{
cout<<"str1>str2"<<endl;
}
return 0;
}
结果:
1.3模拟实现strlen
方式1(临时变量计数器方式):
#include<assert.h>
#include<iostream>
using namespace std;
int my_strlen(const char* arr)
{
assert(arr);//或者assert(arr!=NULL);
int count =0;
while(*arr++)//或者while(*arr++!='\0')
{
count++;
}
return count;
};
方式2(指针方式):
int my_strlen(const char* str)
{
const char* p = str;
while (*p++ );
return p - str -1;
}
2.strcpy
2.1strcpy作用与注意点
strcpy是字符串复制函数,将字符串2复制到字符串1;
函数原型:char* strcpy(char * destination, const char * source );
注意:
- 源字符串必须以 ‘\0’ 结束。
- 会将源字符串中的 ‘\0’ 拷贝到目标空间。
- 目标空间必须足够大,以确保能存放源字符串。
- 目标空间必须可变。
2.2strcpy库函数实现
char * __cdecl strcpy(char * dst, const char * src)
{
char * cp = dst;
while( *cp++ = *src++ ); /* Copy src over dst */
return( dst );
}
2.3strcpy函数的模拟实现
char * my_strcpy(char* dest, const char* str)
{
//检查指针有效性
assert(dest != NULL && str != NULL);
char* s = dest;
//s指向dest起始的地址
//*str赋值给*dest,当找到*dest='\0'的时候,也就是*str为'\0',赋值给*dest的时候,就停止了
while ((*dest++ = *str++) != '\0')
{
NULL;
}
return s;//循环终止,返回s
}
int main()
{
char str1[] = "hello world!";//复制过来需要注意接受的数组的容量必须足够大,或者接受不了,不安全
char str2[] = "xiaobite!";
printf("%s", my_strcpy(str1, str2));
printf("\n%s", str1);
return 0;
}
3.strncpy
3.1strncpy作用与注意点
strncpy作用跟strcpy是一样的,只不过strncpy有长度限制,而strcpy没有长度限制
函数原型:char * strncpy ( char * destination, const char * source, size_t num );
注意
1.拷贝num个字符从源字符串到目标空间。
2.如果源字符串的长度小于num,则拷贝完源字符串之后,在目标的后边追加0,直到num个。
3.2strncpy库函数实现
char * __cdecl strncpy (
char * dest,
const char * source,
size_t count
)
{
char *start = dest;
while (count && (*dest++ = *source++)) /* copy string */
count--;
if (count) /* pad out with zeroes */
while (--count)
*dest++ = '\0';
return(start);
}
3.3strncpy函数的模拟实现
char * my_strncpy(char* dest, const char* str,size_t n)
{
assert(dest != NULL && str != NULL);
char* s = dest;
while (n--)
{
*dest++ = *str++;
}
return s;
}
int main()
{
char str1[] = "hello world!";
char str2[] = "xiaobite!";
printf("%s", my_strncpy(str1, str2, 3));
printf("\n%s", str1);
return 0;
}
4.strcat
4.1strcat作用与注意点
strcat是字符串链接函数,能将字符串2链接在字符串1的后面;
函数原型:char * strcat ( char * destination, const char * source )
注意:
- 源字符串必须以 ‘\0’ 结束。
- 目标空间必须有足够的大,能容纳下源字符串的内容。
- 目标空间必须可修改。
- 字符串自己给自己追加,如何?(这个问题库函数模拟实现后再说)
4.2strcat库函数实现
char * __cdecl strcat (char * dst,const char * src)
{
char * cp = dst;
while( *cp )
cp++; /* find end of dst */
while( *cp++ = *src++ ) ; /* Copy src to end of dst */
return( dst ); /* return dst */
}
4.3strcat函数的模拟实现
char* my_strcat(char* dest, const char* s)
{
//str指向的dets的起始地址
char* str = dest;
//检查指针是否有效
assert(dest && s);
//找到*dest='\0'的时候停止
while (*dest)
{
dest++;
}
//这里再讲*s赋值给*dest,一直找到*s为'\0'为止,最后返回str
while (*dest++ = *s++)
{
;
}
return str;
}
int main()
{
char s1[20] = "hello ";//这里需要注意容量,一般是字符串,弄到字符串数组中,容量一定要够大
char* s2 = "bit.";
my_strcat(s1, s2);
printf("%s", s1);
return 0;
}
5.strncat
5.1 strncat作用与注意点
strncat作用跟strcat作用一样,但也有长度的限制
函数原型:char * strncat ( char * destination, const char * source, size_t num );
5.2strncat库函数实现
char * __cdecl strncat (
char * front,
const char * back,
size_t count
)
{
char *start = front;
while (*front++);
front--;
while (count--)
if (!(*front++ = *back++))
return(start);
*front = '\0';
return(start);
}
5.3strncat函数的模拟实现
char* my_strncat(char* dest, const char* s,size_t n)
{
char* str = dest;
assert(dest && s);
while (*dest)
{
dest++;
}
while (n--)
{
*dest++ = *s++;
}
return str;
}
int main()
{
char s1[20] = "hello ";
char* s2 = "bit.";
my_strncat(s1, s2, 1);
printf("%s", s1);
return 0;
}
6.strcmp
6.1strcmp作用与注意点
strcmp是字符串比较函数,比较字符串s1和s2,如果第一个字母相等,则比较下一个字母,直到找出谁大谁小,或者找到s1和s2相等。
函数原型:int strcmp ( const char * str1, const char * str2 );
注意:
- 第一个字符串大于第二个字符串,则返回大于0的数字
- 第一个字符串等于第二个字符串,则返回0
- 第一个字符串小于第二个字符串,则返回小于0的数字
6.2strcmp库函数实现
int __cdecl strcmp (const char * src,const char * dst)
{
int ret = 0 ;
while( ! (ret = *(unsigned char *)src - *(unsigned char *)dst) && *dst)
++src, ++dst;
if ( ret < 0 )
ret = -1 ;
else if ( ret > 0 )
ret = 1 ;
return( ret );
}
6.3strcmp函数的模拟实现
int my_strcmp(const char* s1, const char* s2)
{
assert(s1 && s2);//检查指针是否有效
while (*s1 == *s2)//当*s1和*s2相等的时候,就继续往后走
{
//记住看while,因为*s1和*s2相等,才到这个if这里来的,注意一下
if (*s1 == '\0')//如果*s1为空,则证明*s1和*s2相等
return 0;
s1++;
s2++;
}
//当*s1和*s2不相等的时候,就直接返回他们的差值
return *s1 - *s2;
}
int main()
{
char *str1 = "abcde";
char *str2 = "abcdef";
int ret = my_strcmp(str1, str2);
if (ret > 0)
printf("str1大");
else if (ret < 0)
printf("str2大");
else
printf("str1与str2相等");
return 0;
}
7.strncmp
7.1strncmp作用与注意点
strncmp函数作用同strcmp一样,只不过也有长度限制
函数原型:int strncmp ( const char * str1, const char * str2, size_t num );
注意:
比较到出现另个字符不一样或者一个字符串结束或者num个字符全部比较完。
7.2strncmp库函数实现
int __cdecl strncmp
(
const char *first,
const char *last,
size_t count
)
{
size_t x = 0;
if (!count)
{
return 0;
}
/*
* This explicit guard needed to deal correctly with boundary
* cases: strings shorter than 4 bytes and strings longer than
* UINT_MAX-4 bytes .
*/
if( count >= 4 )
{
/* unroll by four */
for (; x < count-4; x+=4)
{
first+=4;
last +=4;
if (*(first-4) == 0 || *(first-4) != *(last-4))
{
return(*(unsigned char *)(first-4) - *(unsigned char *)(last-4));
}
if (*(first-3) == 0 || *(first-3) != *(last-3))
{
return(*(unsigned char *)(first-3) - *(unsigned char *)(last-3));
}
if (*(first-2) == 0 || *(first-2) != *(last-2))
{
return(*(unsigned char *)(first-2) - *(unsigned char *)(last-2));
}
if (*(first-1) == 0 || *(first-1) != *(last-1))
{
return(*(unsigned char *)(first-1) - *(unsigned char *)(last-1));
}
}
}
/* residual loop */
for (; x < count; x++)
{
if (*first == 0 || *first != *last)
{
return(*(unsigned char *)first - *(unsigned char *)last);
}
first+=1;
last+=1;
}
return 0;
}
7.3strncmp函数的模拟实现
int my_strncmp(const char* s1, const char* s2,size_t n)
{
assert(s1 && s2);
while (n--)
{
if (*s1 == *s2)
{
if (*s1 == '\0')
return 0;
s1++;
s2++;
}
}
return *s1 - *s2;
}
int main()
{
char *str1 = "abcde";
char *str2 = "ebf";
int ret = my_strncmp(str1, str2,3);
if (ret > 0)
printf("str1大");
else if (ret < 0)
printf("str2大");
else
printf("str1与str2相等");
return 0;
}
七.数据的存储
1.大端存储、小端存储
大端字节序:把数据的低位字节序的内容放到高地址,高位字节序的内容放到低地址;
小端字节序:把数据的低位字节序的内容放到低地址,高位字节序的内容放到高地址。
例题1:设计小程序判断当前机器是大端存储还是小端存储
int main()
{
int a=1;
char* p=(char *)&a;
if(*p==1)
cout<<"小端"<<endl;
else
cout<<"大端"<<endl;
return 0;
}
例题2:整型提升练习
分析输出什么?
int main()
{
char a=-1;
signed char b=-1;
unsigned char c=-1;
cout<<(int)a<<endl;
cout<<(int)b<<endl;
cout<<(int)c<<endl;
return 0;
}
分析:
int main()
{
char a=-1;
//10000000 00000000 00000000 00000001(-1原码)
//11111111 11111111 11111111 11111110(-1反码)
//11111111 11111111 11111111 11111111(-1补码)
//11111111(转换为char类型)
//11111111 11111111 11111111 11111111(有符号整型提升,全补符号位)
signed char b=-1;
//b跟a一样
unsigned char c=-1;
//11111111 11111111 11111111 11111111(-1补码)
//11111111(转换为unsigned char类型)
//00000000 00000000 00000000 11111111(无符号整型提升,全补0)
cout<<(int)a<<endl;
cout<<(int)b<<endl;
cout<<(int)c<<endl;
return 0;
}
结果:(整型提升规则见二.2整型提升)
补充:char在C语言中没规定是有符号字符还是无符号字符;
但int规定了是有符号整形
例题3.整型提升
int main()
{
char a = -128;
printf("%u\n", a);
return 0;
}
分析:
int main()
{
char a = -128;
//10000000 00000000 00000000 10000000(-128原码)
//11111111 11111111 11111111 01111111(-128反码)
//11111111 11111111 11111111 10000000(-128补码)
//10000000(char a)
printf("%u\n", a);
//11111111 11111111 11111111 10000000(有符号整型提升,全补符号位)
//以无符号整型打印,上面这个补码认为是无符号数,就是个正数,最高位的1不是符号位,编译器直接打印这个补码的原码(就是自身)
return 0;
}
结果:
例题4.整型提升
int main()
{
char a = 128;
printf("%u\n", a);
return 0;
}
分析:
int main()
{
char a = 128;
//00000000 00000000 00000000 10000000
//10000000(a)
//11111111 11111111 11111111 10000000(有符号位整型提升,前面补符号位)
//以无符号整型打印,
printf("%u\n", a);
return 0;
}
结果:
char类型数据在计算机中的存储:
例题5.有符号整型和无符号整型类型转换
int main()
{
int i=-20;
unsigned int j=10;
if(i+j>0)
{
printf("大于0");
}
return 0;
}
有符号整型和无符号整型相加放入内存时,有符号整型会强制转换成无符号整型。
例题6.char类型在计算机中的存储情况
int main()
{
char a[1000] = {0};
int i=0;
for(i=0; i<1000; i++)
{
a[i] = -1-i;
}
printf("%d",strlen(a));
return 0;
}
分析:
int main()
{
char a[1000] = {0};
int i=0;
for(i=0; i<1000; i++)
{
a[i] =-1 -i;
}
//-1 -2 -3...-127 -128 127 126 2 1 0 -1 -2 -3...
printf("%d",strlen(a));//strlen()内部指针指向0时,结束循环,所以从-1到-128再从127到1,结果为255
return 0;
}
//size_t __cdecl strlen (const char * str)
//{
// const char *eos = str;
// while( *eos++ ) ;
// return( eos - str - 1 );
//}
char类型数据存储数值范围-128到127
unsigned char类型数据存储数值范围0到255
从limits.h中查到:
例题7.在32位大端模式处理器上变量b等于()
int main()
{
unsigned int a=0x1234;
unsigned char b=*(unsigned char*)&a;
return 0;
}
(大端存储)b:0x00
(小端存储)b:0x34
例题8.5位运动员参加了10米台跳水比赛,有人让他们预测比赛结果
A选手说:B第二,我第三;
B选手说:我第二,E第四;
C选手说:我第一,D第二;
D选手说:C最后,我第三;
E选手说:我第四,A第一;
比赛结束后,每位选手都说对了一半,请编程确定比赛的名次。
int main()
{
int a,b,c,d,e=0;
for(a=1;a<=5;a++)
{
for(b=1;b<=5;b++)
{
for(c=1;c<=5;c++)
{
for(d=1;d<=5;d++)
{
for(e=1;e<=5;e++)
{
if((((b==2)^(a==3))+((b==2)^(e==4))+((c==1)^(d==2))+((c==5)^(d==3))+((e==4)^(a==1))==5)
&&(a*b*c*d*e==120))
{
cout<<"a:"<<a<<endl;;
cout<<"b:"<<b<<endl;;
cout<<"c:"<<c<<endl;;
cout<<"d:"<<d<<endl;;
cout<<"e:"<<e<<endl;
};
}
}
}
}
}
return 0;
}
注意按位异或^和加号+的优先级
2.浮点型的存取方式
从limits.h中查到:
整型:
浮点型:
例题1:为什么是这个结果?
int main()
{
int n = 9;
float* pFloat = (float*)&n;
printf("n的值为:%d\n", n);
printf("*pFloat的值为:%f\n", *pFloat);
*pFloat = 9.0;
printf("num的值为:%d\n", n);
printf("*pFloat的值为:%f\n", *pFloat);
return 0;
}
整型的形式放入,整型的形式取出,没问题;
整型的形式放入,浮点型的形式取出,有问题;
浮点型的形式放入,浮点型的形式取出,没问题;
浮点型的形式放入,整型的形式取出,有问题。
说明浮点数和整数在内存中存储的方式一定是有区别的。
根据IEEE标准,任何二进制浮点数可以表示为以下形式:
(-1)^ S* M *2^E
(-1)^S表示符号位,当s=0,V为正数;当s=1,V为负数。
M表示有效数字,大于等于1,且小于2
2^E表示指数位
浮点数“5.5"在计算机内部是如何存储的?
为什么是1-127(或者1-1023)?因为这个1是把1.xxxxxx整数位拿到指数上去了,后面小数是0.xxxxxx。个人倾向于不拿整数位,直接按自己的方式算,结果一样。
分析下5.5的浮点型是如何存储的?
int main()
{
float f = 5.5f;
//101.1
//1.011 * 2^2
//s=0 M=1.011 E=2
//s=0 M=011 E=2+127
//
//0 10000001 011 0000 0000 0000 0000 0000
//0100 0000 1011 0000 0000 0000 0000 0000
//40 b0 00 00
return 0;
}
再看例题1:
int main()
{
int n = 9;
//00000000 00000000 00000000 00001001
float* pFloat = (float*)&n;
printf("n的值为:%d\n", n);//9
printf("*pFloat的值为:%f\n", *pFloat);
//00000000 00000000 00000000 00001001
//0 00000000 00000000000000000001001
//s=0;E=1-127=-126;M=0.00000000000000000001001
//0.00000000000000000001001*2^(-126)极趋近于0
//故打印出0.000000
*pFloat = 9.0;
//浮点数转二进制
//1001.0
//1.001*2^3
//s=0;E=3+127=130;M=001
//0 10000010 00100000000000000000000
printf("num的值为:%d\n", n);//1,091,567,616;见下图
printf("*pFloat的值为:%f\n", *pFloat);//9.0
return 0;
}
结果:
八.运算符重载汇总
#include<iostream>
using namespace std;
class Person{//友元,用于访问此类内部private属性
friend Person operator+(Person &p1,Person&p2);
friend ostream& operator<<(ostream& out,Person& po);
friend Person operator+(Person& pE,int a);
public:
Person(){
m_age=0;
m_b=0;
}
Person(int a,int b){
m_age=a;
m_b=b;
}
//前置自增运算符重载
Person& operator++(){
this->m_age++;
this->m_b++;
return *this;
};
//后置自增运算符重载
Person operator++(int){
Person temp;
temp.m_age=m_age;
temp.m_b=m_b;
m_age++;
m_b++;
return temp;
};
private:
int m_age;
int m_b;
};
//加号重载:p1+p2
Person operator+(Person &p1,Person&p2){
Person p3;
p3.m_age=p1.m_age+p2.m_age;
p3.m_b=p1.m_b+p2.m_b;
return p3;
};
//加号重载:p1+int
Person operator+(Person& pE,int a){
Person temp;
temp.m_age=pE.m_age+a;
temp.m_b=pE.m_b+a;
return temp;
};
//左移运算符重载:<<
ostream& operator<<(ostream& out,Person& po){
out<<"m_age:"<<po.m_age<<" m_b:"<<po.m_b<<endl;
return out;
};
int main()
{
Person pA(10,10);
Person pB(20,20);
Person pC=pA+pB;
Person pD=pC+10;
cout<<"pC:"<<pC<<"pD:"<<pD<<endl;
cout<<++(++pD)<<endl;//前置自增返回引用,可以链式前置自增
//cout<<(pD++)++<<endl;//后置自增返回值,无法链式后置自增
return 0;
}