第一节——分支语句和循环语句
if与else匹配过程中产生:悬空else问题
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
int main()
{
int a = 0;
int b = 2;
if (a == 1)
if (b == 2)
printf("hehe\n");//不打印
else
printf("haha\n");//不打印
return 0;
}
- else会匹配上面离它最近的if
解决方案一
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
int main()
{
int a = 0;
int b = 2;
if(a == 1)
{
if(b == 2)
{
printf("hehe\n");
}
}
else
{
printf("haha\n");
}
return 0;
}
- 加上{ }之后就可以避免悬空else的问题,且代码的可读性也会大大增加,
编程好习惯:if和else中的代码尽量写在代码块里{ } (具体问题具体分析)
switch语句的正确使用
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
int main()
{
int day = 0;
scanf("%d", &day);
switch (day)
{
case 1:
printf("星期1\n");
break;
case 2:
printf("星期2\n");
break;
}
default:
break;
return 0;
}
编程好习惯:
- 在switch语句中每个case后面都尽量写上break(具体问题具体分析)
- 在switch语句的最后放一条default字句,并在后面加上一个break
对default的正确解释
- 每个switch语句中只能出现一条default子句。 但是它可以出现在语句列表的任意位置,而且语句流会像执行每一个case标签一样执行default子句。
- 当switch表达式的值并不匹配所有 case 标签的值时,这个 default 子句后面的语句就会执行,
while语句的正确使用
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
int main()
{
int ch = 0;
while ((ch = getchar()) != EOF)
{
putchar(ch);
}
return 0;
}
- 这段代码会从键盘中一直读取字符,直到文件结束EOF
- getchar:输入一个字符
- putchar:输出一个字符
对EOF的正确解释
- 为End Of File的缩写,通常在文本的最后存在,用这个字符来表示文本结束
- ASCII代码值的范围是0~127,不可能出现-1,所以EOF在C语言中为-1,在while循环中以EOF作为文件结束标志,
使用getchar所产生的问题
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
int main()
{
char password[20] = { 0 };
printf("请输入密码:>");
scanf("%s", password);//123456
printf("请确认密码(Y/N):>");
char ch = getchar();
if (ch == 'Y')
printf("确认成功\n");
else
printf("确认失败\n");
return 0;
}
- 本质上getchar是从缓冲区中获得一个字符(缓冲区拿字符,会大大的提高效率)
- 回车是会触发\n的,这就会导致getchar直接读取\n确认失败,也不会让我们确认密码(Y/N)
解决方案一
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
int main()
{
char password[20] = { 0 };
printf("请输入密码:>");
scanf("%s", password);//123456
int tmp = 0;
while ((tmp = getchar()) != '\n') {
;
}
printf("请确认密码(Y/N):>");
char ch = getchar();
if (ch == 'Y')
printf("确认成功\n");
else
printf("确认失败\n");
return 0;
}
- 它会清除所有字符直到\n,并且\n也会
for循环的经典坑点
坑点一
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
int main()
{
int i = 0;
for (i = 0; i < 3; i++)
{
printf("%d ", i);
int j = 0;
for (j = 0; j < 3; i++)
{
printf("hehe\n");//死循环
}
}
return 0;
}
- 这个坑点就是在一个循环体内改变另一个循环体的变量
- 这种错误的代码书写是有可能导致死循环的,自己书写的时候一定要注意
坑点二
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
int main()
{
for (int i = 0;;i++)
{
printf("hehe");//死循环
}
return 0;
}
- 这个坑点就是for循环省略判断语句
- 这种情况就会导致判断恒为真,循环永不退出,死循环,自己书写的时候一定要注意
坑点三:这样的循环要执行多少次
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
int main()
{
int i = 0;
int k = 0;
for (i = 0, k = 0; k = 0; i++, k++)
k++;
return 0;
}
- 这个地方的坑点在for循环中的判断条件,这里写的是k = 0(这是一条赋值语句),
而不是k == 0(判断语句) - 这段代码中的循环语句一次都不会执行,自己书写的时候要注意=与==的区别
不推荐使用的goto语句
首先声明:理论上goto语句是没有必要的,实践中没有goto语句也可以很容易的写出代码,
goto语句的使用
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
int main()
{
char input[20] = { 0 };
system("shutdown -s -t 60");
tag:
printf("请注意,你的电脑将在1分钟内关机,如果输入:我是猪,就取消关机\n");
scanf("%s", input);
if (strcmp(input, "我是猪") == 0)
{
system("shutdown -a");
return 0;
}
goto tag;
return 0;
}
- goto语句一般要用标记跳转的标号来配合使用,以达到跳转的目的
不使用goto语句
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
int main()
{
char input[20] = { 0 };
system("shutdown -s -t 60");
while (1)
{
printf("请注意,你的电脑在1分钟内关机,如果输入:我是猪,就取消关机\n");
scanf("%s", input);
if (strcmp(input, "我是猪") == 0)
{
system("shutdown -a");
break;
}
}
return 0;
}
- 这里的while写法是能和goto写法达到同一个效果的,
对goto语句的补充说明
- 在某些特殊的场合下goto语句还是用得着的,最常见的用法就是终止程序在某些深度嵌套的结构的处理过程。比如在写大型的实战项目的时候
- 又比如一次跳出两层或多层循环。这时用break是达不到目的的。break只能从当前层循环跳到上一层的循环。这时goto语句就很有用了,goto语句能跳出一下子跳出多层循环
- 注意:goto语句不能跨函数更不能跨文件使用,
第二节——函数
为什么会有库函数?
- 为了支持可移植性和提高程序的效率,所以C语言的基础库中提供了一系列类似的库函数,方便程序员进行软件开发。
不常见但有点用的库函数
模拟实现字符串拷贝:strcpy
char * strcpy ( char * destination , const char * source );
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#include<assert.h>
#include<string.h>
char* my_strcpy(char* des, const char* sou)
{
assert(des && sou);
char* ret = des;
while (*des++ = *sou++)
{
;
}
return ret;
}
int main()
{
char arr1[20] = { "xxxxxxxxxxxxxxxxx" };
char arr2[] = { "hello" };
printf("%s\n", my_strcpy(arr1, arr2));//链式访问
printf("%s\n", strcpy(arr1, arr2));//链式访问
return 0;
}
- 注意strcpy拷贝的时候会把'\0'一起拷贝过去
模拟实现求字符串的长度:strlen
size_t strlen ( const char * str)
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#include<assert.h>
#include<string.h>
//计数器
int my_strlen1(const char* str)
{
assert(str);
int count = 0;
while (*str++ != '\0') {
count++;
}
return count;
}
//指针-指针
int my_strlen2(const char* str)
{
assert(str);
const char* ret = str;
while (*str != '\0') {
str++;
}
return str - ret;
}
//函数递归
int my_strlen3(const char* str)
{
assert(str);
if (*str == '\0'){
return 0;
}
return my_strlen3(++str) + 1;
}
int main()
{
char arr[] = "abcdefg";
printf("%d\n", my_strlen3(arr));
printf("%d\n", strlen(arr));
return 0;
}
模拟实现字符串比较:strcmp
int strcmp ( const char * str1, const char * str2)
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#include<assert.h>
#include<string.h>
int my_strcmp(const char* s1, const char* s2)
{
assert(s1 && s2);//断言
while (*s1 == *s2)
{
if (*s1 == '\0')
{
//当读取到\0结束时
return 0;
}
s1++;
s2++;
}
return *s1 - *s2;
}
int main()
{
char arr1[] = "abce afd";
char arr2[] = "abce f";
if (!my_strcmp(arr1, arr2)) {
printf("两个字符串相同\n");
}
else if (!strcmp(arr1, arr2)) {
printf("两个字符串相同\n");
}
else {
printf("两个字符串不同\n");
}
return 0;
}
模拟实现字符串追加:strcat
char * strcat (char *destination , const char* soure) ;
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#include<assert.h>
#include<string.h>
char* my_strcat(char* dest, const char* src)
{
char* ret = dest;
assert(dest && src);
//1. 找目标字符串中的\0
while (*dest){
dest++;
}
//2. 追加源字符串,包含\0
while (*dest++ = *src++){
;
}
return ret;
}
int main()
{
char arr1[20] = "abc ";
char arr2[] = "eeffff";
printf("%s\n", my_strcat(arr1, arr2));
printf("%s\n", strcat(arr1, arr2));
return 0;
}
模拟实现字符串查找:strstr
const char * strstr (const char * str1, const char * str2);
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#include<assert.h>
#include<string.h>
char* my_strstr(const char* str1, const char* str2)
{
assert(str1 && str2);
if (*str2 == '\0')//如果查找的是空字符
{
return str1;
}
const char* first1 = str1;
const char* first2 = str2;
while (*str1 != '\0')
{
if (*str1 != *str2) {
str1 = ++first1;
str2 = first2;
}
else {
str1++;
str2++;
}
if (*str2 == '\0') {
return first1;
}
}
return NULL;
}
int main()
{
char arr1[] = "abbcdef";
char arr2[] = "bcd";
if (my_strstr(arr1, arr2))
{
printf("%s\n", my_strstr(arr1, arr2));
}
if (strstr(arr1, arr2)) {
printf("%s\n", strstr(arr1, arr2));
}
else {
printf("不存在\n");
}
return 0;
}
补充说明:下面三个库函数中的num都是字节数
模拟实现内存拷贝:memcpy
void *memcpy (void *destnation, const void * source,size_t num );
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#include<assert.h>
#include<string.h>
void* my_memcpy(void* des, const void* cou, size_t num)
{
assert(des && cou);
void* ret = des;//记录地址
while (num--)
{
*(char*)des = *(char*)cou;
des = (char*)des + 1;
cou = (char*)cou + 1;
}
return ret;
}
int main()
{
int arr1[] = { 1,2,3,4,5,6,7,8,9,10 };
int arr2[] = { 1,2,3,4,5,6,7,8,9,10 };
int sz = sizeof(arr1) / sizeof(arr1[0]);
int i = 0;
my_memcpy(arr1 + 2, arr1, 20);
for (i = 0; i < sz; i++)
{
printf("%d ", arr1[i]);
}
printf("\n");
memcpy(arr2 + 2, arr2, 20);
//memmove(arr2 + 2, arr2, 20);
for (i = 0; i < sz; i++)
{
printf("%d ", arr2[i]);
}
return 0;
}
- void *能接收所有的指针,char* 的指针加1,就是加的一个字节
- 在C语言中memcpy是不能自己拷贝自己的(重叠的内存块),自己拷贝自己有一个更安全的库函数memmove,
- 注意:在vs2019中memcpy是可以自己拷贝自己的,即memcpy的功能和memmove的功能相同
模拟实现内存移动:memmove
void *memmove(void *destination,const void*source ,size_t num );
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#include<assert.h>
#include<string.h>
void* my_memmove(void* des, const void* cou, size_t num)
{
assert(des && cou);
void* ret = des;//记录地址
//从前开始移动
if (des < cou) {
while (num--)
{
*(char*)des = *(char*)cou;
des = (char*)des + 1;
cou = (char*)cou + 1;
}
}
//从后开始移动
else {
while (num--)
{
*((char*)des + num) = *((char*)cou + num);
}
}
return ret;
}
int main()
{
int arr1[] = { 1,2,3,4,5,6,7,8,9,10 };
int arr2[] = { 1,2,3,4,5,6,7,8,9,10 };
int sz = sizeof(arr1) / sizeof(arr1[0]);
int i = 0;
my_memmove(arr1 + 2, arr1, 20);
memmove(arr2 + 2, arr2, 20);
for (i = 0; i < sz; i++)
{
printf("%d ", arr1[i]);
}
printf("\n");
for (i = 0; i < sz; i++)
{
printf("%d ", arr2[i]);
}
return 0;
}
模拟实现内存比较:memcmp
int memcmp (const void * ptr1,const void * ptr2,size_t num);
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#include<assert.h>
#include<string.h>
int my_memcmp(const void* ptr1, const void* ptr2, size_t num)
{
assert(ptr1 && ptr2);
while (num--)
{
if(*(char*)ptr1 != *(char*)ptr2)
{
return *(char*)ptr1 - *(char*)ptr2;
}
ptr1 = (char*)ptr1 + 1;
ptr2 = (char*)ptr2 + 1;
}
return 0;
}
int main()
{
int arr1[] = { 1,2,3,4,5,6,7,8,9,10 };
int arr2[] = { 1,2,2,3,4,5,6,7,7,2 };
int sz = my_memcmp(arr1, arr2,20);
if (sz > 0) {
printf("arr1>arr2\n");
}
else if (sz < 0) {
printf("arr1<arr2\n");
}
else {
printf("arr1==arr2\n");
}
return 0;
}
模拟实现将字符串转换成整型的库函数:atol
模拟实现qsort函数
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
int int_cmp(const void* e1, const void* e2)//这个函数是需要使用者自己确定的。
{
return (*(int*)e1 - *(int*)e2);//如果交换e1和e2的值,将进行降序
}
void _swap(void* p1, void* p2, int size)
{
//有了元素的类型,才能一个字节一个字节的访问
int i = 0;
for (i = 0; i < size; i++)
{
char tmp = *((char*)p1 + i);
*((char*)p1 + i) = *((char*)p2 + i);
*((char*)p2 + i) = tmp;
}
}
void qsort_bubble(void* base, int count, int size, \
int(*cmp)(void* e1, void* e2))
{
int i = 0;
int j = 0;
for (i = 0; i < count - 1; i++)
{
for (j = 0; j < count - i - 1; j++)
{
//上面两个循环就像冒泡排序的一样的设计思路
if (cmp((char*)base + j * size, \
(char*)base + (j + 1) * size) > 0)//比较两个元素的大小
{
//交换两个元素的位置,
_swap((char*)base + j * size, \
(char*)base + (j + 1) * size, size);//需要传入元素的类型,
}
}
}
}
int main()
{
int arr[] = { 1, 3, 5, 7, 9, 2, 4, 6, 8, 0 };
//char *arr[] = {"aaaa","dddd","cccc","bbbb"};
int i = 0;
qsort_bubble(arr, sizeof(arr) / sizeof(arr[0]), sizeof(int), int_cmp);
for (i = 0; i < sizeof(arr) / sizeof(arr[0]); i++)
{
printf("%d ", arr[i]);
}
printf("\n");
return 0;
}
其中这段代码有两个灵魂设计点
第一个就是:传入函数参数用void*,然后又通过char*进行一个字节一个字节的访问,比如
第二个就是使用了回调函数
对函数嵌套调用的正确理解
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
void test3()
{
printf("hehe\n");//hehe
}
int test2()
{
test3();
return 0;
}
int main()
{
test2();
return 0;
}
- 函数与函数之间的相互组合,相互调用就叫做嵌套调用
- 注意:函数与函数之间能嵌套调用,但是不能嵌套定义
对函数链式访问的正确理解
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<string.h>
int main()
{
int len = strlen("abc");
printf("%d\n", len);
//链式访问
printf("%d\n", strlen("abc"));
return 0;
}
- 把一个函数的返回值作为另外一个函数的参数就叫做链式访问
对函数声明的正确理解
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
int main()
{
int a = 10;
int b = 20;
//函数声明一下 - 告知
int Add(int x, int y);
int c = Add(a, b);
printf("%d\n", c);
return 0;
}
//函数的定义
int Add(int x, int y)
{
return x + y;
}
- 函数的使用是需要保证先声明后调用(防止编译器报出函数未定义的错误)
- 编译器的扫描顺序是从上向下的,如果函数的定义在调用之前就不用声明了
对函数递归的深入理解
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
void print(unsigned int n)//123
{
if (n > 9)
{
print(n / 10);
}
printf("%d ", n % 10);//1 2 3
}
int main()
{
unsigned int num = 0;
scanf("%u", &num);//123
//递归 - 函数自己调用自己
print(num);//print函数可以打印参数部分数字的每一位
return 0;
}
- 函数自己嵌套自己,自己调用自己就叫做函数递归
一个正确的递归需要两个条件
-
一个限制条件,当满足这个限制条件的时候,递归便不再继续。
-
一个接近限制条件的条件,每次递归都接近限制条件
只要不理解函数递归,就画递归展开图
写递归代码的几个要求
- 不能写死递归,必须要有跳出条件,
- 每次递归都应该逼近跳出条件,
- 递归层次不能太深,防止栈溢出
=====================================================
函数递归的几个经典题目之汉诺塔
函数递归的几个经典题目之青蛙跳台阶
=========================================================================
对函数递归的补充说明
1. 许多问题是以递归的形式进行解释的,这只是因为它比非递归的形式更为清晰。
2. 但是这些问题的迭代实现往往比递归实现效率更高,虽然代码的可读性稍微差些。
3. 当一个问题相当复杂,难以用迭代实现时,递归实现的简洁性便可以补偿它所带来的运行时开销
第三节——数组
对变长数组的本质理解
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
int main()
{
int n = 10;
int arr[n] = { 0 };
return 0;
}
- C99引入了变长数组的概念,可以给[ ] 中加一个变量才可以
- C99之前是没有变长数组的,[ ] 只能加一个常量
小细节tips:数组越界
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
printf("%d\n", arr[10]);
printf("%d\n", arr[11]);
printf("%d\n", arr[12]);
return 0;
}
- 数组越界是不会报错的,但有可能会报警告
- 对于数组越界的问题,需要程序员自己检查
第四节——操作符
小细节tips:算数操作符
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
int main()
{
int a1 = 6 / 5;
printf("%d\n", a1);//1
float a2 = 6 / 5;
printf("%f\n", a2);//1.000000
float a3 = 6.0 / 5.0;
printf("%f\n", a3);//1.200000
return 0;
}
- 算数操作符有:+ - * / %
- 除了 % 操作符之外,其他的几个操作符都可以作用于整数和浮点数。
- 对于 / 操作符如果两个操作数都为整数,执行整数除法。而只要有浮点数执行的就是浮点数除法。/ 操作符左右两边至少有一个为小数,结果才为小数,比如6/5=1,6/5.0=1.2
- 注意:%不能用于浮点数
补充说明:如果想用printf打印出%,只能用%%进行转换
左移操作符:<< && 右移操作符:<<
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
int main()
{
int a = 2;
//把a的二进制位向左移动一位
int b = a << 1;
printf("b = %d\n", b);//4
printf("a = %d\n", a);//2
int c = -1;
//把c的二进制位向右移动一位
int d = c >> 1;
printf("c = %d\n", c);//-1
printf("d = %d\n", d);//-1
return 0;
}
- 左移操作符移位规则:左边抛弃、右边补0
- 右移操作符移位规则:
- 逻辑移位:左边用0填充,右边丢弃
- 算术移位:左边用原该的符号位填充,右边丢弃
- 在vs2019中对于右移操作符是采用的算术移位
补充说明:对于移位运算符,不要移动负数位,这个是标准未定义的
经典面试题
要求:不能创建临时变量(第三个变量),实现两个数的交换
代码一
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
int main()
{
int a = 3;
int b = 5;
int c = 0;//临时变量
printf("交换前:a = %d b = %d\n", a, b);//交换前:a = 3 b = 5
c = a;
a = b;
b = c;
printf("交换后:a = %d b = %d\n", a, b);//交换后:a = 5 b = 3
return 0;
}
代码二
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
int main()
{
int a = 3;
int b = 5;
printf("交换前:a = %d b = %d\n", a, b);//交换前:a = 3 b = 5
//数值太大会溢出
a = a + b;
b = a - b;
a = a - b;
printf("交换后:a = %d b = %d\n", a, b);//交换后:a = 5 b = 3
return 0;
}
代码三
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
int main()
{
int a = 3;
int b = 5;
//交换
printf("交换前:a=%d b=%d\n", a, b);//交换前:a = 3 b = 5
a = a ^ b;
b = a ^ b;
a = a ^ b;
printf("交换后:a=%d b=%d\n", a, b);//交换后:a = 5 b = 3
return 0;
}
- 代码一的问题:它不符合题目的要求,题目上说不能创建临时变量(第三个变量)
- 代码二的问题:代码二中存在 + 操作符 ,就有可能导致栈溢出
- 代码三完美解决:^ 操作符,二进制中相同为0,相异为1
- 一个数^它本身=0,比如2^2 = 0
- 一个数^0=它本身,比如2^0 = 2
操作符中的短路问题
#include <stdio.h>
int main()
{
int i = 0, a = 0, b = 2, c = 3, d = 4;
i = a++ && ++b && d++;
//i = a++||++b||d++;
printf("a = %d\nb = %d\nc = %d\nd = %d\n", a, b, c, d);
return 0;
}
- 在生活中,有这样的一个例子,如果明天星期一,且不下雨,我就去上班,而现在周二,明天周三,那我还关不关心明天下不下雨,很明显不关心了,因为明天不是周一
- 同理,这里代码中的a++;它是先使用再加加的,所以在一堆&&中,从开头就为假了,之后的就不用看了
C语言中唯一的三目操作符
exp1 ? exp2 : exp3
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
int main()
{
int a = 3;
int b = 0;
if (a > 5)
b = 1;
else
b = -1;
printf("%d\n", b);//-1
//三目操作符
b = (a > 5 ? 1 : -1);//-1
printf("%d\n", b);
return 0;
}
- exp1为真,结果就是exp2,反之结果就是exp3
- 在可读性上:条件操作符的确更简洁一点,
逗号表达式
exp1 , exp2 , exp3 , …expN
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
int main()
{
int a = 1;
int b = 2;
int c = (a > b, a = b + 10, a, b = a + 1);//逗号表达式
printf("%d\n", c);//13
return 0;
}
- 逗号表达式,就是用逗号隔开的多个表达式,从左向右依次执行。整个表达式的结果是最后一个表达式的结果。
隐式类型转化
隐式类型转化又叫做整形提升, 至于为什么会发生整形提升,解释起来很麻烦,一句话
表达式中各种长度可能小于int长度的整型值,都必须先转换为int或unsigned int, 然后才能送入 CPU 去执行运算
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
int main()
{
char a = 3;
char b = 127;
char c = a + b;
printf("%d\n", c); //-126
return 0;
}
- 这里的a b c三个都是char,都没有达到int,所以一定会发生整形提升
- 整形提升是按照原符号位进行提升的
案例一
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
int main()
{
char c = 1;
printf("%u\n", sizeof(c));//1
printf("%u\n", sizeof(+c));//4
printf("%u\n", sizeof(-c));//4
printf("%u\n", sizeof(!c));//4 gcc - 4
return 0;
}
- 在语法上这段代码中的sizeof(!c),结果应该是4,但在vs2019中结果是1,我认为这个像是vs的一个bug
- Linux下的gcc编译器对sizeof(!c)的处理结果是4
算数转换
第一位 | long double |
第二位 | double |
第三位 | float |
第四位 | unsigned long int |
第五位 | long int |
第六位 | unsigned int |
第七位 | int |
如果某个操作数的类型在上面这个列表中排名较低
那么首先要转换为另外一个操作数的类型后执行运算
比如:
- 一个整数乘以一个小数,结果是小数,这就发生了算术转换,
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
int main()
{
float f = 3.14;
int num = f;//隐式转换,会有精度丢失
return 0;
}
- 这段代码存在潜在的问题,
- 注意:算术转换都是向着精度更高的转换,
第五节——指针
对指针的大小的理解
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
int main()
{
int* pa;
char* pc;
float* pf;
printf("%d\n", sizeof(pa));//4或8
printf("%d\n", sizeof(pc));//4或8
printf("%d\n", sizeof(pf));//4或8
return 0;
}
-
指针的大小跟指针的类型无关,取决于机器的是32位的,还是64位的
-
在32位的机器下指针的大小为4,在64位机器下指针的大小为8
指针类型的意义
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
int main()
{
int arr[10] = { 0 };
int* p = arr;
char* pc = (char*)arr;
printf("%p\n", p);
printf("%p\n", p + 1);
printf("%p\n", pc);
printf("%p\n", pc + 1);
return 0;
}
- 指针的类型决定了指针向前或者向后走一步有多大(距离)
野指针问题
定义:
- 野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)
原因
-
指针未初始化
-
指针越界访问
-
指针指向的空间释放
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#include<stdlib.h>
int main()
{
//1.未初始化
int* p;//p野指针
*p = 20;
//2.越界访问
int arr[10] = { 0 };
int* ps = arr;
int i = 0;
for (i = 0; i <= 10; i++)
{
*ps = i;//当i = 10,ps野指针
ps++;
}
//3.指向的空间释放
int* tmp = (int*)malloc(sizeof(int) * 2);
free(tmp);
int* pt = tmp;//tmp野指针
return 0;
}
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
int* test()
{
int a = 10;
return &a;
}
int main()
{
int*p = test();
*p = 20;
return 0;
}
- 这里的p也是一个野指针,因为p指向的空间释放已经被释放了
如何规避野指针
- 指针初始化
- 小心指针越界
- 指针指向空间释放及时置 NULL
- 避免返回局部变量的地址
- 指针使用之前检查
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
int main()
{
//当前不知道p应该初始化为什么地址的时候,直接初始化为NULL
int* pa = NULL;
//明确知道初始化的值
int a = 10;
int* pc = &a;
int* p = NULL;
if(p != NULL)
*p = 10;
return 0;
}
- C语言本身是不会检查数据的越界行为的,但编译器还是会报警告
指针-指针
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
char c[5];
printf("%d\n", &arr[9] - &c[0]);//err
printf("%d\n", &arr[9] - &arr[0]);//9
return 0;
}
- 指针和指针相减的前提:两个指针指向同一块空间
- 指针-指针得到的是中间的元素个数,
补充:指针+指针没有意义,就像日期+日期一样没意义,所以不讨论
小细节tips:指针的关系运算
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
int main()
{
int arr[5] = { 1,2,3,4,5 };
int* ps = NULL;
//允许指向数组元素的指针与指向数组最后一个元素
//后面的那个内存位置的指针比较
for (ps = arr; ps < &arr[5]; ps++)
{
*ps = 0;
}
//但是不允许与指向第一个元素之前的那个内存位置的指针进行比较。
for (ps = &arr[4]; ps > &arr[-1]; ps--)
{
*ps = 0;
}
return 0;
}
- 实际在绝大部分的编译器上是可以顺利完成任务的,然而我们还是应该避免这样写,因为标准并不保证它可行。
- 标准规定:允许指向数组元素的指针与指向数组最后一个元素后面的那个内存位置的指针比较,但是不允许与指向第一个元素之前的那个内存位置的指针进行比较。
第六节——结构体
结构体访问的三种方式
- 操作符有:. * ->
结构体传参的两种方式
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
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;
}
- 这里有两种传参的方式,一种是传值调用,一种是传址调用
- 推荐使用传址调用,理由如下
- 函数传参的时候,参数是需要压栈的。
- 如果传递一个结构体对象的时候,结构体过大,
参数压栈的的系统开销比较大,会导致性能的下降。
第七节——预处理
首先我们要知道程序翻译的过程:预处理->编译->汇编->连接
其中的预处理包括:
- 头文件的展开
- 宏替换
- 去注释
- 条件编译
证明宏替换和去注释的先后顺序
- 预处理期间先执行去注释,后进行宏替换
用 define 宏定义表式时的坑点
- 这段代码中的宏替换的时候,替换了多条语句,而在没有{ }的情况下,if和else只能匹配一条语句,所以这里会报错
解决方案
- 在这里其实是可以把宏定义中的语句放进{ }里,但是我上面这样做是最完美的
- 结论:当我们需要宏进行多语句替换的时候, 推荐使用do-while-zero结构
小细节tips:宏定义中的空格
- 在#define和符号之间是可以有空格的(第七行),没有影响,但是尽量不要这样写
小细节tips:宏的有效范围&&定义
- 宏的有效范围:是从定义处往下有效,之前无效
- 在源文件的任何地方,宏都可以定义,与是否在函数内外无关
#undef 的本质作用
- #undef是取消宏的意思,主要用于限定宏的有效范围。
演示#ifdef &&#ifndef
- 这两个条件编译能通过裁剪代码,快速实现版本维护(free,收费),功能裁剪,跨平台性等等
- 一个前面定义了宏,就执行后面的语句,另一个前面没有定义宏,就执行后面语句
#if 的本质作用
- #if是用来判断宏的真假,主要用于处理条件编译中多条件的情况
其他案例
- #if defined也是支持嵌套的
补充说明:#if defined等价于#ifdef,#if !defined等价于#ifndef
头文件被重复包含的情况
- 这种情况会引起多次拷贝,主要会影响编译效率!同时,也可能引起一些未定义错误,但是特别少,自己书写时也要尽可能避免头文件被重复包含
解决方案
- 这是通过条件编译来解决头文件被重复包含的情况
- 我更常用的是#pragma once,主要是因为方便
#error 预处理
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
//#define __cplusplus
int main()
{
#ifndef __cplusplus
#error 老铁,你用的不是C++的编译器哦
#endif
return 0;
}
- #error的核心作用是可以进行自定义编译报错。还可以定制化文件名称和代码行号
#line 预处理
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
int main()
{
printf("%s, %d\n", __FILE__, __LINE__);
#line 60 "hehe.h" //定制化完成
printf("%s, %d\n", __FILE__, __LINE__);
return 0;
}
- _FILE_当前文件的文件名,_LINE_当前代码的行号
- #line 定制化文件名称和代码行号
#pragme 预处理
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#define M 10
int main()
{
#ifdef M
#pragma message("M宏已经被定义了")
#endif
return 0;
}
- #pragma message()作用:在编译中打印信息
特殊的 # 运算符
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <string.h>
#define TOSTRING(s) #s
int main()
{
int abc = 12345;
char str[64] = { 0 };
strcpy(str, TOSTRING(abc));
printf("%s", str);
return 0;
}
- 宏中使用# 就是将内容转换成为"字符串"
特殊的 ## 运算符
- 宏中使用## 就是将内容连接成一个新的字符