指针是个变量,存放内存单元的地址(编号)
int main(){
int a = 10; // 在内存中开辟一块儿空间
int *p = &a; // 这里我们对变量a,取出它的地址,可以使用&操作符
//将a的地址存放在p变量种,p就是一个指针变量
}
指针在32位平台上是4个字节,在64位平台上是8个字节。
char * p = 'a';
int a = 10;
printf("%d\n",sizeof(char *));//4
printf("%d\n", sizeof(short *));//4
printf("%d\n", sizeof(int *));//4
printf("%d\n", sizeof(long *));//4
printf("%d\n", sizeof(float *));//4
printf("%d\n", sizeof(double *));//4
指针的0
值就是NULL
int a = 0;
int *p = NULL;//0 0地址
assert(p != NULL);
//断言 -》 一般用户debug模式下
//一旦 断言失败 程序将不会继续执行
if (p != NULL)
{
*p = 100;
printf("%d\n", *p);//0号地址 是不可以进行访问的
}
else
{
printf("指针为空\n");
}
在使用指针之前,需要进行一个判断,判断是否指针指向为NULL
,可以使用上述代码中的assert()
和if
两个任意部分都可以进行判断。
野指针:①指针未初始化;②指针越界访问;③指针指向的空间释放
指针运算
指针+ -整组
#include<stdio.h>
int main(){
int n = 10;
char *pc = (char*)&n;
int *pi = &n;
printf("%p\n", &n); //009EFC24
printf("%p\n", pc); //009EFC24
printf("%p\n", pc+1); //009EFC25
printf("%p\n", pi); // 009EFC24
printf("%p\n", pi+1); // 009EFC28
return 0;
} 地址变化情况如上所示
指针部分习题汇总
练习1:指针部分概念补充:
①指针是变量,用来存放地址,指向同类型的一块内存空间。
②地址是唯一的,一个指针变量中只能存储一个地址,因此可以唯一指向内存中的一块区域。
③野指针指向的空间是非法的,或者说该指针指向的空间已经不存在了,因此野指针不能使用。
局部指针变量没有初始化时里面就是随机值,因此指向的那个位置不一定,故将其看成时野指针。
练习2:
在32位系统下,int
类型占4个字节,指针表示地址空间个数,总共有2^32
个,故占4个字节。
在64位系统下,int
类型占4个字节,指针表示地址空间个数,总共有2^64
个,故占8个字节。
练习3下面代码的运行结果
int main(){
int arr[] = { 1, 2, 3, 4, 5 }; //定义一个整型数组
short *p = (short*)arr; //将原本int型数组的指针,转换成short类型的指针,p记录了数组arr的地址
// 只是此时地址从int类型转换成了short类型
//short*类型,因此p每次只能所有两个字节
int i = 0;
for (i = 0; i < 4; i++){
*(p + i) = 0;
}
for (i = 0; i<5; i++)
{
printf("%d ", arr[i]);
}
return 0;
}
解析:
arr数组在内存中的存储格式为:
0x00ECFBF4: 01 00 00 00
0x00ECFBF8: 02 00 00 00
0x00ECFBFC: 03 00 00 00
0x00ECFC00: 04 00 00 00
0x00ECFC04: 05 00 00 00
指针p的类型为short*类型的,因此p每次只能所有两个字节,for循环对数组中内容进行修改时,一次访问的是:
arr[0]的低两个字节,arr[0]的高两个字节,arr[1]的低两个字节,arr[1]的高两个字节,故改变之后,数组中内容如下:
0x00ECFBF4: 00 00 00 00
0x00ECFBF8: 00 00 00 00
0x00ECFBFC: 03 00 00 00
0x00ECFC00: 04 00 00 00
0x00ECFC04: 05 00 00 00
故最后打印:0 0 3 4 5
练习4下面代码的运行结果
int main(){
unsigned long pulArray[] = { 6, 7, 8, 9, 10 };
unsigned long *pulPtr;
pulPtr = pulArray; //将pulArray数组的第一个元素的地址赋给pulPtr
//*(pulPtr + 3) += 3;
*(pulPtr + 3) = *(pulPtr + 3) + 3;// pulptr+3访问的是数组中第三个元素(数组下标从0开始),故将9改为9+3=12
printf("%d,%d\n", *pulPtr, *(pulPtr + 3));// 打印第一个和第三个元素,因此:打印6和12
}
练习5:关于二级指针的概念补充:
①二级指针指向的空间中存储的也是一个地址,用来保存一级指针的地址
②数组的地址一般用一级指针存储,或使用数组指针接受
③二级指针也是指针,所占字节数有具体系统决定。
练习6:关于指针运算相关概念补充,对于整形指针(int*)类型:
①整形指针+1,向后偏移一个整型类型的大小,即4个字节。
②两个指针相减,指针必须指向一段连续空间,减完之后的结构代表两个指针之间相差元素的个数
③整形指向的是一个整形的空间,解引用操作访问4个字节
④指针中存储的是地址,地址可以看成一个数据,因此是可以比较大小的。
练习7下面代码的输出结果:
#include <stdio.h>
int main()
{
int a = 0x11223344;
char *pc = (char*)&a;
*pc = 0;
printf("%x\n", a);
return 0;
}
解析:
假设,a变量的地址为0x64,则a变量在内存中的模型为:
0x64| 44 |
0x65| 33 |
0x66| 22 |
0x67| 11 |
char*类型的指针变量pc指向只能指向字符类型的空间,如果是非char类型的空间,必须要将该空间的地址强转为char*类型。
char *pc = (char*)&a; pc实际指向的是整形变量a的空间,即pc的内容为0x64,即44,
*pc=0,即将44位置中内容改为0,修改完成之后,a中内容为:0x11223300
练习8:关于指针的一些概念补充:
①int * const p
中,const
修饰指针变量p
本身,表示p
的指向不能改变。
②int const *p
与const int *p
中,const
修饰p
指针解引用之后的结果,表示p
指向的内容不能改变。
③int *p[10]
定义了一个指针数组,数组中10个元素,每个元素都是int*
类型的指针
④int (*p)[10]
定义了一个数组指针该指针只能指向存储10个整形元素的数组。
练习9:使用指针打印数组内容,写一个函数打印arr数组的内容,不使用数组下标,使用指针。arr是一个整形一维数组。
#include<stdio.h>
#include<windows.h>
void Arr_Print(int arr[], int size){
//分析:因为数组中存储的元素类型是int类型的,因此只要给一个int的指针,依次取索引数组中的每个元素即可
int *p = arr;
for (int i = 0; i < size; i++){
printf("%d\n", *(p + i));
//printf("%d ", *p); // *p: 取到p所指向位置的元素
//++p;
}
}
int main(){
int arr1[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9 };
int size = sizeof(arr1) / sizeof(arr1[0]);
Arr_Print(arr1, size);
system("pause");
}
练习10:字符串逆序,写一个函数,可以逆序一个字符串的内容。
#include<stdio.h>
#include<windows.h>
void Reverse(char* str){
char* left = str;
char* right = strlen(str) - 1;
while (left < right){
char temp = *left;
*left = *right;
*right = temp;
++left;
--right;
}
}
int main(){
char str[] = "abcdef";
Reverse(str);
system("pause");
return 0;
}
//使用循环输入的方法
int main()
{
char str[101] = {0};
while(gets(str))
{
Reverse(str);
printf("%s\n", str);
memset(str, 0, sizeof(str)/sizeof(str[0]));
}
return 0;
}
练习11:计算求和,求Sn = a + aa + aaa + aaaa + aaaaa 的前5项之和,其中a是一个数字,例如:2 + 22 + 222 + 2222 + 22222
#include<stdio.h>
#include<stdlib.h>
int main(){
int a;
int sum = 0;
int add = 0;
printf("请输入一个1-9的数字:\n");
scanf("%d", &a);
if (a >= 10 || a <= 0){
return 0;
}
for (int i = 0; i < 5; i++){
add = add * 10 + a;
sum = sum + add;
}
printf("sum=%d\n", sum);
system("pause");
return 0;
}
练习12:水仙花数
求出0~100000之间的所有“水仙花数”并输出。
水仙花数”是指一个n位数,其各位数字的n次方之和确好等于该数本身,
如:153=1 ^ 3+5 ^ 3+3 ^ 3,则153是一个“水仙花数”。
此题的关键在于只要知道判断一个数据是否为水仙花数的方式,问题就迎刃而解。
思路:
假定给定一个数据data,具体检测方式如下:
1. 求取data是几位数
2. 获取data中每个位置上的数据,并对其进行立方求和
3. 对data中每个位上的数据立方求和完成后,在检测其结果是否与data相等即可,
相等:则为水仙花数
否则:不是
#include<stdio.h>
#include<math.h>
int main(){
for (int i = 0; i < 100000; i++){
int count = 1; //记录有几位数
int tmp = i;
int sum = 0;
//判断i是否为水仙花数
//第1步:求判断数字的位数
while (tmp / 10){
count++;
tmp = tmp / 10;
}
//第2步:计算每一位的次方和
tmp = i;
while (tmp){
sum += pow(tmp % 10, count);
tmp = tmp / 10;
}
//第3步:判断
if (sum == i){
printf("%d ", i);
}
}
return 0;
}
练习13:打印菱形,用C语言在屏幕上输出以下图案:
思路:
仔细观察图形,可以发现,此图形中是由空格和*按照不同个数的输出组成的。
上三角:先输出空格,后输出*,每行中
空格:从上往下,一行减少一个
*:2*i+1的方式递增
下三角:先输出空格,后输出*,每行中
空格:从上往下,每行多一个空格
*: 从上往下,按照2*(line-1-i)-1的方式减少,其中:line表示总共有多少行
按照上述方式,将上三角和下三角中的空格和*分别输出即可。
#include<stdio.h>
int main(){
int line = 7;
//首先打印上半部分
for (int i = 0; i < line; i++){
//打印一行
//打印空格
for (int j = 0; j < line-1-i; j++){
printf(" ");
}
//打印*
for (int j = 0; j < 2 * i + 1; j++){
printf("*");
}
printf("\n");
}
//接着打印下半部分
for (int i = 0; i < line - 1; i++){
//打印一行
for (int j = 0; j < i; j++){
printf(" ");
}
for (int j = 0; j < 2 * (line - 1 - i); j++){
printf("*");
}
printf("\n");
}
return 0;
}
练习14:喝汽水问题
喝汽水,1瓶汽水1元,2个空瓶可以换一瓶汽水,给20元,可以多少汽水(编程实现)。
思路:
1. 20元首先可以喝20瓶,此时手中有20个空瓶子
2. 两个空瓶子可以喝一瓶,喝完之后,空瓶子剩余:empty/2(两个空瓶子换的喝完后产生的瓶子) + empty%2(不够换的瓶子)
3. 如果瓶子个数超过1个,可以继续换,即重复2
int main()
{
int money = 0;
int total = 0;
int empty = 0;
scanf("%d", &money);
//方法1
total = money;
empty = money;
while(empty>1)
{
total += empty/2;
empty = empty/2+empty%2;
}
return 0;
}
// 方法二:按照上述喝水和用瓶子换的规则的话,可以发现,其实就是个等差数列:money*2-1
int main()
{
int money = 0;
int total = 0;
int empty = 0;
scanf("%d", &money);
//方法2
if(money <= 0)
{
total = 0;
}
else
{
total = money*2-1;
}
printf("total = %d\n", total);
return 0;
}
练习15模拟实现库函数strlen
int MyStrlen(char *str){
int count = 0;
assert(str != NULL);
while (*str != 0){ //当不为\0时
count++;
str++;
}
return count;
}
int main(){
char str[] = "hello bit";
int ret = MyStrlen(str);
printf("%d", ret);
}
练习16:模拟实现库函数strcpy。此函数是将字符串s2拷贝到s1中,连同字符串结束标志也一同拷贝。如果s2=“good”,那么内存合适的s1中存放的是good\0。
#include<stdio.h>
#include<assert.h>
char *my_strcpy(char *dest, char *scr)
{
char *p = dest;
assert(dest != NULL);//断言,会打印出一条错误信息
assert(scr != NULL);
while (*dest++ = *scr++){
}
return p;
}
int main()
{
char p1[] = "helloworld";
char p2[] = "good";
my_strcpy(p1, p2);
printf("%s\n", p1);
system("pause");
return 0;
练习17:调整奇数偶数顺序,调整数组使奇数全部都位于偶数前面
输入一个整数数组,实现一个函数,来调整该数组中数字的顺序使得数组中所有的奇数位于数组的前半部分,所有偶数位于数组的后半部分。
思路:
1. 给定两个下标left和right,left放在数组的起始位置,right放在数组中最后一个元素的位置
2. 循环进行一下操作
a. 如果left和right表示的区间[left, right]有效,进行b,否则结束循环
b. left从前往后找,找到一个偶数后停止
c. right从后往前找,找到一个奇数后停止
d. 如果left和right都找到了对应的数据,则交换,继续a
void swap_arr(int arr[], int sz)
{
int left = 0;
int right = sz - 1;
int tmp = 0;
while (left<right)
{
// 从前往后,找到一个偶数,找到后停止
while ((left<right) && (arr[left] % 2 == 0))
{
left++;
}
// 从后往前找,找一个奇数,找到后停止
while ((left<right) && (arr[right] % 2 == 1))
{
right--;
}
// 如果偶数和奇数都找到,交换这两个数据的位置
// 然后继续找,直到两个指针相遇
if (left<right)
{
tmp = arr[left];
arr[left] = arr[right];
arr[right] = tmp;
}
}
}
void Print(int* p, int right)
{
int i = 0;
for (i = 0; i <= right; i++)
{
printf("%d ", *(p + i));
}
printf("\n");
}
int main()
{
int arr[] = { 1, 3, 2, 4, 6, 7, 8, 9, 10, 11 };
int sz = sizeof(arr) / sizeof(arr[0]) - 1;
swap_arr(arr, sz);
int right = sz - 1;
Print(arr, right);
return 0;
}
结构体部分习题汇总
练习1:对于一下结构体:
struct student
{
int num;
char name[32];
float score;
}stu;
①在C语言中需要自定义类型时,要用到struct关键字
②在C语言中,用strcut定义的结构体,定义结构体类型变量时,需要用strcut student
③在上述结构体中,stu是定义的结构体类型变量,不是名称,如需定义名称,要在结构体定义是添加typedef
关键字
练习2:
结构体类型变量需要访问其成员时,用运算符,如果是指向结构体变量的指针访问时,需要使用->
或先对指针解引用,取到指向的结构体类型的变量,再通过.
访问。在以下结构体中,可以填入横线以访问成员a的有a.a , p->a , (*p).a
#include < stdio.h >
struct S
{
int a;
int b;
};
int main( )
{
struct S a, *p=&a;
a.a = 99;
printf( "%d\n", __________);
return 0;
}
练习3:下面程序的输出结果:
struct stu{
int num;
char name[10];
int age;
};
void fun(struct stu *p){
printf("%s\n", (*p).name);
return;
}
int main(){
struct stu students[3] = { { 9801, "zhan", 20 },
{ 9802, "wang", 19 },
{ 9803, "zhao", 18 }
};
fun(students + 1);
return 0;
} //将会打印出wang
调试部分习题汇总
练习1:C语言常见错误包含:编译错误;连接错误;运行时错误(其中包含了栈溢出)。
练习2:关于VS调试快捷键:
①F5是开始调试,在遇到短点的位置可以停下来,Ctrl+F5是开始执行,不调试
②F10-是逐过程调试,遇到函数不进入函数
③F11-是逐语句调试,可以观察调试的每个细节
④F9会在光标所在行下短点,如果有短点会取消短点
练习3:关于Debug和Relese的区别:
①Debug为调试版本,一般在开发完成后发布工程前,调试代码都是在Debug模式下进行的
②Release版本最终要发送给用户的,发送给用户的版本必须要没有问题,测试人员就是最后一个把关的
③Debug版本是调试版本,编译器编译时会增加一些调试信息,编译器基本不会对其进行优化
④Release版本虽然也可以调试,但没有意义,因为编译器的优化太大了,一般都是在Debug版本下进行调试,Release版本一般编译器会进行大量的优化,删除无用的代码,指令的次序调整等,使速度更快。
练习4:VS开发环境调试下面的代码,画图解释下面代码的问题
#include <stdio.h>
int main()
{
int i = 0;
int arr[] = {1,2,3,4,5,6,7,8,9,10};
for(i=0; i<=12; i++)
{
arr[i] = 0;
printf("hello bit\n");
}
return 0;
}
答案解析:
以下代码有两个问题:1. 数组访问越界 2. 死循环
以下代码再vs2013下会造成死循环,原因:
栈内存:
|CC CC CC CC|
arr[0]|01 00 00 00|\
arr[1]|02 00 00 00| \
arr[2]|03 00 00 00| \
arr[3]|04 00 00 00| \
arr[4]|05 00 00 00| \
arr[5]|06 00 00 00| / arr的空间
arr[6]|07 00 00 00| /
arr[7]|08 00 00 00| /
arr[8]|09 00 00 00| /
arr[9]|0A 00 00 00|/
|CC CC CC CC|
|CC CC CC CC|
|00 00 00 00| i的空间
|CC CC CC CC|
for循环中,i的内容是从0,一直增加到12,而数组只有10个空间,因此会越界
每次访问arr数组i号位置时,都会将该位置内容设置为0,当访问到arr[12]时,也会将该位置内容设置为0,而位置恰好为i的位置,即a[12]恰巧将i设置为0,因此造成死循环。