搜索

C语言教程

编程基础

通俗地理解什么是编程语言 C 语言究竟是一门怎样的语言? C 语言和 C++ 到底有什么关系? 学了 C 语言到底能做什么,能从事什么工作? 二进制、八进制和十六进制 不同进制之间的转换 数据在内存中的存储 载入内存,让程序运行起来 ASCII 编码,将英文存储到计算机 GB2312 编码和 GBK 编码,将中文存储到计算机 Unicode 字符集,将全世界的文字存储到计算机 程序员的薪水和发展方向大全 不要这样学习 C 语言,这是一个坑! 明白了这点才能学好编程,否则参加什么培训班都没用

C 语言初探

第一个 C 语言程序 选择正确的输入法,严格区分中英文 什么是源文件? 什么是编译和链接? 主流 C 语言编译器有哪些? 什么是 IDE(集成开发环境)? 什么是工程/项目? 哪款 C 语言编译器(IDE)适合初学者? 如何在手机上编写 C 语言代码? C 语言的三套标准:C89、C99 和 C11 C 语言为什么有那么多编译器? 程序安装是怎么回事? 制作安装包,让用户安装程序 C 语言程序的错误和警告 分析第一个 C 语言程序 C 语言代码中的空白符 彩色版的 C 语言,让文字更漂亮 一个真正带界面的 C 语言程序

变量和数据类型

大话 C 语言变量和数据类型 在屏幕上输出各种类型的数据 C 语言中的整数(short,int,long) 二进制数、八进制数和十六进制数 C 语言中的正负数及其输出 整数在内存中是如何存储的,为什么它堪称天才般的设计 整数的取值范围以及数值溢出 C 语言中的小数(float,double) 小数在内存中是如何存储的,揭秘诺贝尔奖级别的设计(长篇神文) 在 C 语言中使用英文字符 在 C 语言中使用中文字符 C 语言到底使用什么编码?谁说 C 语言使用 ASCII 码,真是荒谬! C 语言转义字符 C 语言中的几个重要概念 C 语言加减乘除运算 C 语言自增(++) 和自减(--) 变量的定义位置以及初始值 运算符的优先级和结合性 C 语言数据类型转换

C 语言输入输出

数据输出大汇总以及轻量进阶 在屏幕的任意位置输出字符,开发小游戏的第一步 使用 scanf 读取从键盘输入的数据 从键盘输入字符和字符串 进入缓冲区(缓存)的世界,破解一切与输入输出有关的疑难杂症 结合缓冲区谈 scanf 函数,那些奇怪的行为其实都有章可循 清空(刷新)缓冲区,从根本上消除那些奇怪的行为 scanf 的高级用法,原来 scanf 还有这么多新技能 C 语言模拟密码输入(显示星号 ) 非阻塞式键盘监听,用户不输入数据程序也能继续执行

循环结构和选择结构

else 语句 C 语言关系运算符 C 语言逻辑运算符 case 语句 C 语言条件运算符 C 语言 while 循环 C 语言 for 循环 C 语言跳出循环 C 语言循环嵌套 对选择结构和循环结构的总结 谈编程思维的培养,初学者如何实现自我突破(非常重要) 写一个内存泄露的例子,让计算机内存爆满

C 语言数组

什么是数组 C 语言二维数组 判断数组中是否包含某个元素 C 语言字符数组和字符串 字符串的输入和输出 C 语言字符串处理函数 C 语言数组是静态的,不能插入或删除元素 C 语言数组的越界和溢出 C 语言变长数组:使用变量指明数组的长度 对数组元素进行排序 对 C 语言数组的总结

C 语言函数

什么是函数? C 语言函数定义 C 语言函数的形参和实参 C 语言函数的返回值 C 语言函数的调用(从中发现程序运行的秘密) 函数声明以及函数原型 全局变量和局部变量 C 语言变量的作用域 C 语言块级变量 C 语言递归函数(带实例演示) 中间递归函数(比较复杂的一种递归) 多层递归函数(最烧脑的一种递归) 递归函数的致命缺陷:巨大的时间开销和内存开销(附带优化方案) 忽略语法细节,从整体上理解函数

预处理命令

什么是预处理命令? #include 的用法 C 语言宏定义 C 语言带参数的宏定义 带参宏定义和函数的区别 宏参数的字符串化和宏参数的连接 C 语言中几个预定义宏 C 语言条件编译 #error 命令,阻止程序编译 C 语言预处理命令总结

指针

1 分钟彻底理解指针的概念 指针变量的定义和使用 C 语言指针变量的运算 数组指针(指向数组的指针) 字符串指针(指向字符串的指针) C 语言数组灵活多变的访问形式 指针变量作为函数参数 C 语言指针作为函数返回值 二级指针(指向指针的指针) 空指针 NULL 以及 void 指针 数组和指针绝不等价,数组是另外一种类型 数组到底在什么时候会转换为指针 指针数组(数组每个元素都是指针) 一道题目玩转指针数组和二级指针 二维数组指针(指向二维数组的指针) 函数指针(指向函数的指针) 只需一招,彻底攻克 C 语言指针,再复杂的指针都不怕 main() 函数的高级用法:接收用户输入的数据 对 C 语言指针的总结

结构体

C 语言结构体 C 语言结构体数组 C 语言结构体指针 C 语言枚举类型 C 语言共用体 大端小端以及判别方式 C 语言位域 C 语言位运算 使用位运算对数据或文件内容进行加密

重要知识点补充

typedef 的用法 const 的用法 C 语言随机数

文件操作

C 语言中的文件是什么? C 语言打开文件 文本文件和二进制文件到底有什么区别? 以字符形式读写文件 以字符串的形式读写文件 以数据块的形式读写文件 格式化读写文件 随机读写文件 C 语言实现文件复制功能 FILE 结构体以及缓冲区深入探讨 获取文件大小(长度) 插入、删除、更改文件内容

C 语言调试

调试的概念以及调试器的选择 设置断点,开始调试 查看和修改变量的值 单步调试(逐语句调试和逐过程调试) 即时窗口的使用 查看、修改运行时的内存 有条件断点的设置 assert 断言函数 调试信息的输出 VS 调试的总结以及技巧

assert 断言函数

在我们的实际开发过程之中,常常会出现一些隐藏得很深的 BUG,或者是一些概率性发生的 BUG,通常这些 BUG 在我们调试的过程中不会出现很明显的问题,但是如果我们将其发布,在用户的各种运行环境下,这些程序可能就会露出马脚了。那么,如何让我们的程序更明显的暴露出问题呢?这种情况下,我们一般都会使用 assert 断言函数,这是 C 语言标准库提供的一个函数,也就是说,它的使用与操作系统平台,调试器种类无关。我们只要学会了它的使用,便可一次使用,处处开花。

接下来我们来了解一下 assert 函数的用法,这个函数在 assert.h 头文件中被定义,在微软的 cl 编译器中它的原型是这样的:
#define assert(_Expression) (void)( (!!(_Expression)) || (_wassert(_CRT_WIDE(#_Expression), _CRT_WIDE(__FILE__), __LINE__), 0) )
我们看到 assert 在 cl 编译器中被包装成了一个宏,实际调用的函数是 _wassert ,不过在一些编译器中,assert 就是一个函数。为了避免这些编译器差异带来的麻烦,我们就统一将 assert 当成一个函数使用。

我们来了解一下 assert 函数的用法和运行机制,assert 函数的用法很简单,我们只要传入一个表达式即可,它会计算我们传入的表达式的结果,如果为真,则不会有任何操作,但是如果我们传入的表达式的计算结果为假,它就会像  stderr (标准错误输出)打印一条错误信息,然后调用 abort 函数直接终止程序的运行。

现在我们在 Visual Studio 中创建一个工程,输入下面这段非常简单的代码:
#include <stdio.h>
#include <assert.h>
int main()
{
    printf("assert 函数测试:");
    assert(true); //表达式为真
    assert(1 >= 2); //表达式为假
    return 0;
}
我们按 F5 启动调试:

我们看到,我们的输出窗口打印出了断言失败的信息,并且 Visual Studio 弹出了一个对话框询问我们是否继续执行。但是如果我们不绑定调试器,构建发布版程序,按 Ctrl + F5 直接运行呢?是的,这个 assert 语句就无效了,原因其实很简单,我们看看 assert.h 头文件中的这部分代码:
#ifdef NDEBUG
#define assert(_Expression)     ((void)0)
#else  /* NDEBUG */
我们看到,只要我们定义了 NDEBUG 宏,assert 就会失效,而 Visual Studio 的默认的发布版程序编译参数中定义了 NDEBUG 宏,所以我们不用额外定义,但是在其他编译器中,我们在发布程序的时候就必须在包含 assert.h 头文件前定义 NDEBUG 宏,避免 assert 生效,否则总是让用户看到“程序已经停止运行,正在寻找解决方案 . . .”的 Windows 系统对话框可就不妙了。

下面我们来了解一下 assert 的常用情境以及一些注意事项。举个 C 语言文件操作的例子:
#include <stdio.h>
#include <assert.h>
#include <stdlib.h>

int main(void)
{
    FILE *fpoint;
    fpoint = fopen("存在的文件.txt", "w");
    //以可读写的方式打开一个文件
    //如果文件不存在就自动创建一个同名文件
    assert(fpoint);                        
    //(第一次断言)所以这里断言成功
    fclose(fpoint);
    fpoint = fopen("不存在的文件.txt", "r");
    //以只读的方式打开一个文件,如果不存在就打开文件失败
    assert(fpoint);
    //(第二次断言)所以断言失败
    printf("文件打开成功");
    //程序永远都执行不到这里来
    fclose(fpoint);                          
    return 0;
}
阅读代码,我们可以知道,如果我们错误的设置了读写权限,或者没有考虑 Windows 7(或更高版本 ) 的 UAC(用户账户权限控制 ) 问题的话,我们的程序出现的问题就很容易在我们的测试中暴露出来。事实上,上面的代码也不是一定能通过第一次断言的,我们把构建生成的(Debug 模式的)可执行文件复制到我们的系统盘的 Program Files 文件夹再执行,那么 Win7 及以上的操作系统的 UAC 都会导致程序无法通过第一次断言的。所以在这种情况下我们就需要在必要的情况申请管理员权限避免程序 BUG 的发生。

在我们的实际使用过程中,我们需要注意一些使用 assert 的问题。首先我们看看下面的这个断言语句:
//...
assert( c1 /*条件1*/ && c2 /*条件2*/ );
//...
我们思考一下:如果我们的程序在这里断言失败了,我们如何知道是 c1 断言失败还是 c2 断言失败呢,答案是:没有办法。在这里我们应该遵循使用 assert 函数的第一个原则:每次断言只能检验一个条件,所以上面的代码应该改成这样:
//...
assert(c1 /*条件1*/);
assert(c2 /*条件2*/);
//...
这样,一旦出现问题,我们就可以通过行号知道是哪个条件断言失败了。

接下来我们来思考一个问题:我们可以使用 assert 语句代替我们的条件过滤语句么?我们来举个例子,我们写了一个视频格式转换器,我们本应输出标准 MP4 格式的视频,但是由于编码解码库的问题,我们在转换过程中出现了错误,像这种情况下,我们能否用 assert 代替掉我们的 if 语句呢(假设这里忽略了 Visual Studio 发布版程序编译过程中自动定义的 NDEBUG 宏)?很明显,这样做是不可以的。这是为什么呢?

现在我们来区分一下程序中的错误与异常,所谓错误,是代码编写途中的缺陷导致的,是程序运行中可以用 if 语句检测出来的。而异常在我们的 C 语言中,一般也可以使用 if 语句判断,并进行对应的处理。而 assert 是用来判断我们程序中的代码级别的错误的。像用户输入错误,调用其他函数库错误,这些问题我们都是可以用 if 语句检测处理的。另一方面,使用 if 语句的程序对用户更友好。

下面我们通过代码展示使用 assert 的另外一个注意意事项,我们新建一个工程,输入如下代码:
#include <stdio.h>
#include <assert.h>
int main(void)
{
    int i = 0;
    for ( ; ; )
    {
        assert(i++ <= 100);
        printf("我是第%d行\n",i);
    }
    return 0;
}
我们按 F5 运行调试器,我们会看到这样的情景:

这是正常的,我们按 Shift + F5 终止调试,接下来,我们切换一下编译模式到发布模式:

接下来我们按 Ctrl+F5 不绑定调试器直接运行:

我们看到了一个完全不相同的运行结果,这是为什么呢?其实原因很简单,我们注意这段代码:
assert(i++ <= 100);
我们的条件表达式为 i++ <= 100,这个表达式会更改我们的运行环境(变量 i 的值 ),在发布版程序中,所有的 assert 语句都会失效,那么这条语句也就被忽略了,但是我们可以把它改为 i++ ; assert(i <= 100); ,这样程序就能正常运行了。所以请记住:不要使用会改变环境的语句作为断言函数的参数,这可能导致实际运行中出现问题。

最后,我们再来探讨一下,什么时候应该用 assert 语句?一个健壮的程序,都会有 30%~50%的错误处理代码,几乎用不上 assert 断言函数,我们应该将 assert 用到那些极少发生的问题下,比如 Object* pObject = new Object,返回空指针,这一般都是指针内存分配出错导致的,不是我们可以控制的。这时即使你使用了容错语句,后面的代码也不一定能够正常运行,所以我们也就只能停止运行报错了。

意见反馈

微信二维码 微信扫码关注 抖音二维码 抖音扫码关注