精华内容
下载资源
问答
  • Q:萌新求助,为啥不开 -O2 能 AC,开了 -O2 就 RE/TLE/WA 了?A:你的程序可能有 UB。Q:萌新求助,为啥本地数据能过,提交...在 C/C++ 中,未定义行为的例子非常地多,我们在下文将会提到一些例子。1 和其他概念的...

    94a2b71708aedd950880bae0c6c2913b.png
    Q:萌新求助,为啥不开 -O2 能 AC,开了 -O2 就 RE/TLE/WA 了?
    A:你的程序可能有 UB。
    Q:萌新求助,为啥本地数据能过,提交就全 RE/TLE/WA 了?
    A:你的程序可能有 UB。
    Q:所以啥是 UB?

    未定义行为(Undefined Behavior,UB),是一类对程序无任何限制的行为。

    在 C/C++ 中,未定义行为的例子非常地多,我们在下文将会提到一些例子。

    1 和其他概念的辨析

    在 C/C++ 中,和未定义行为容易混淆的概念有两个,实现定义行为未指明行为。这里先对这几个概念做一个辨析。

    • 实现定义行为:程序的行为随实现而变动,遵从标准的实现必须为每个这样的行为的效果提供文档。一个例子是 int 在不同环境下的大小(标准规定为至少 16 位,现在大多数环境下均为 32 位)。
    • 未指明行为:程序的行为随实现而变动,而不要求遵从标准的实现为每个行为的效果提供文档。虽然行为在变动,但它产生的结果均应该是合法的。一个例子是变量分配的方式和位置(可以把一次定义的不同变量分配到一片连续的空间,当然也可以分开分配)。
    • 未定义行为:对程序的行为无任何限制。前两类行为的结果都要求是合法的,而对于未定义行为,则不要求程序做任何合法,有意义的事情。一个例子是访问非法内存。

    2 为什么会有未定义行为?

    一个正常的 C/C++ 程序的行为都应该是合法的,像未定义行为这样的操作不应该在程序中出现。

    有人也许会奇怪:那为什么不去检测未定义行为,将未定义行为视为语法或语义错误而终止编译呢?

    事实上对未定义行为的检测并没有检测语法和语义错误那么容易。有些未定义行为的检测比较容易(比如访问未初始化的变量),但诸如带符号整数溢出这样的行为,因为它并不一定会发生,在编译阶段检测它就困难不少。

    而假如编译器要考虑这些未定义行为的话,则不利于程序优化。

    因此将一些操作指定为未定义行为,编译器就不必再考虑这些操作,从而利于程序优化。

    这也是一些程序在没有优化的情况下行为正常,开启优化选项之后出现不期待的结果的原因。

    3 一些未定义行为的例子

    3.1 带符号整数算术溢出

    #include <iostream>
    using namespace std;
    int main()
    {
     int x;
     cin>>x;
     if(x+1<x)
      cout<<"Overflow!"<<endl;
     else
      cout<<"Not overflow!"<<endl;
     return 0;
    }

    试着输入 2,147,483,6472147483647 ( 2^{31}-1231−1 )看看程序结果吧!

    如果你编译的时候开启了 -O2 优化选项,或者你的编译器版本比较高的话,你可能会发现预期的输出 Overflow! 没有出现。

    为什么呢?因为带符号整数溢出是未定义行为,从而编译器不会考虑这种情况。在忽略这种情况的前提下,x+1<x 一定为假,从而上面这段程序事实上与下面这段程序等价:

    #include <iostream>
    using namespace std;
    int main()
    {
     int x;
     cin>>x;
     cout<<"Not overflow!"<<endl;
     return 0;
    }

    你也许会奇怪,为什么不将带符号整数溢出的行为给一个明确的定义呢?

    事实上,给带符号整数溢出下定义会带来很多不必要的开销。

    我们来看下面这个程序:

    #include <iostream>
    using namespace std;
    int main()
    {
     int x;
     cin>>x;
     cout<<x*2/2<<endl;
     return 0;
    }

    如果带符号整数溢出是有定义的,那我们就要老实执行一次乘法和一次除法运算。

    而把它定为未定义行为,编译器就可以直接优化成这样:

    #include <iostream>
    using namespace std;
    int main()
    {
     int x;
     cin>>x;
     cout<<x<<endl;
     return 0;
    }

    瞬间减少了不少开销,对吧?当操作次数很多的时候效果当然更加明显。

    需要注意的是,无符号整数溢出不是未定义行为。也就是说:

    920585a7969b202b03ba41f7808c7ab5.png
    #include <iostream>
    using namespace std;
    int main()
    {
     unsigned x;
     cin>>x;
     if(x+1<x)
      cout<<"Overflow!"<<endl;
     else
      cout<<"Not overflow!"<<endl;
     return 0;
    }

    如果你输入 4,294,967,2954294967295 ( 2^{32}-1232−1 ),会发现它的行为是符合预期的(输出 Overflow!)。

    3.2 越界访问

    越界访问是一件让人很头大的事情。

    众所周知,C/C++ 并不会进行越界检查,因此越界访问造成的后果可能有这几种:

    • 访问非法内存而导致程序运行时错误(RE);
    • 意外访问程序里的其他变量导致 暴力写挂;

    为啥不进行越界检查呢?在多数情况下,越界检查的成本并不小,而且进行了越界检查会丧失不少程序优化的机会。因此最划算的决定就是不进行越界检查。

    3.3 无可视副作用的无限循环

    在开始讲这个之前,先引出 C++ 的一个原则,如同原则(as if),这是不少优化的基础。

    简单来说,在不影响程序运行结果的前提下,允许编译器进行一些代码转换。当然,未定义行为造成的影响是例外,因为这种行为不会被编译器考虑。

    因此,可以通过消除冗余代码来达到优化的目的。

    因为无可视副作用的无限循环是未定义行为,编译器有时可以直接将它优化掉。

    #include <iostream>
    using namespace std;
    int f()
    {
     unsigned cnt=0;
     while(1)
      if(cnt<0)return true;
     return false;
    }
    int main()
    {
     if(f())//停机问题(大雾
      cout<<"This program has been terminated."<<endl;
     else
      cout<<"Some strange things happened!"<<endl;
     return 0;
    }

    在 g++ 8.3 下进行编译,程序进入了死循环,而在 clang++ 6.0.0 下进行编译,程序则无输出终止。

    查了下汇编代码,发现 main() 函数在 clang++ 下被优化成了空。

    也就是说,这一无限循环因为是未定义行为,而被编译器视为冗余代码而消除。

    3.4 无法确定的运算顺序

    现在据说还有不少教材还在考这种奇怪的东西。

    #include <iostream>
    using namespace std;
    int main()
    {
     int x=1;
     cout<<(x++ + ++x)<<endl;
     return 0;
    }

    根据 C++11 起的 “按顺序早于”规则,这种式子用一句比较拗口的话来说,就是:标量对象上的一项副作用相对于同一标量对象上的另一副作用为无顺序,则其行为未定义。

    在这个式子中,x++ 的副作用和 ++x 的副作用相比,可以在前面发生,也可以在后面发生,因此它是未定义的。

    3.5 访问未初始化变量

    #include <iostream>
    using namespace std;
    int main()
    {
     int x;
     if(x)
      cout<<"True"<<endl;
     if(!x)
      cout<<"False"<<endl;
     return 0;
    }

    据说在比较旧的 g++ 版本上编译会出现两个都输出的奇怪现象。不过这既然是未定义行为,怎么样的输出都是合理的。

    4 如何检测未定义行为?

    在编译期检测未定义行为难度并不小,原因在上文已经说的很清楚了。

    不过在运行期,还是有一些方法来捕捉程序的未定义行为的。这将在一定程度上方便我们的调试。

    一种非常实用的检测未定义行为的方式是使用 clang 的诊断模式。

    只需在编译选项中加上一条:-fsanitize=undefined 即可开启。

    (注:Linux 下的 g++ 也提供这一编译选项)

    #include <iostream>
    using namespace std;
    int a[10005];
    int main()
    {
     if(a[10005])
      cout<<42<<endl;
     return 0;
    }

    对于上面这个出现数组越界的程序,运行的结果将是:

    a.cpp:6:5: runtime error: index 10005 out of bounds for type 'int [10005]'

    PS:Codeforces 也支持了这个功能,可以在 Custom Test 中选择 Clang++17 Diagnostics 开启。

    5 总结

    作为 C/C++ 的一大特色,未定义行为让不少人都头疼不已。未定义行为不可预测的特点,使调试的难度加大了不少。

    避开未定义行为的关键是养成良好的编程习惯。当然一些辅助的检测手段对于消除未定义行为也能起到非常大的帮助。

    Reference

    • 未定义行为 - cppreference.com
    • 每个 C 程序员都该知道关于未定义行为的事 #1/3
    • 每个 C 程序员都该知道关于未定义行为的事 #2/3
    • 每个 C 程序员都该知道关于未定义行为的事 #3/3

    本文搬运至洛谷日报#265


    搬运不易,举起你可爱的小爪点个赞再走叭?>w<

    展开全文
  • 与MATLAB相比,C/C++的执行效率更高,占用内存更小,如果能够将占用大量时间的核心代码用C/C++实现,编译成动态可执行文件后,在MATLAB中直接调用,就能够提高MATLAB程序的运行速度。不同版本的MATLAB支持的C/C++...

    与MATLAB相比,C/C++的执行效率更高,占用内存更小,如果能够将占用大量时间的核心代码用C/C++实现,编译成动态可执行文件后,在MATLAB中直接调用,就能够提高MATLAB程序的运行速度。

    不同版本的MATLAB支持的C/C++编译器不同,具体可以参考官网链接。下面的代码MATLAB版本为R2018b,使用的C/C++编译器为Visual Studio 2015 Professional。

    MATLAB为C/C++/C++ 11/Fortan/Java/.NET/Python语言留有API接口,

    External Language Interfacesww2.mathworks.cn

    针对C/C++,首先,安装编译器

    mex 

    也可以不在MATLAB中进行编译,而是在VS 2015中编译好后直接拷贝到MATLAB运行文件夹中使用,相对比较复杂,但可以进行调试,具体参考之间的文章

    易夕:MATLAB + VS 2015混合编程并调试zhuanlan.zhihu.com
    8bb327f8b77d78e679568aa792c49460.png

    关于如何编写C/C++的MEX函数,参考官方文档,

    Calling MATLAB from Cww2.mathworks.cn

    下面用一个简单的例子说明C/C++的威力,设有一个有序数组,数组中有一些重复的值,返回去重后的结果。MATLAB 中可以用uniqu函数实现,对于长度为1e9的int32数组,去重需要11.527秒。

    A 

    如果用C实现,采用原地算法,mexFunction代码如下

    include "

    保存为removeDuplicates.cpp,在MATLAB中执行以下命令进行编译

    mex 

    当前文件夹会生成一个removeDuplicates.mexw64文件。现在,可以将removeDuplicates在MATLAB中当作一个函数使用,对长度为1e9的int32数组,去重仅需要2.014秒,相当于原来六分之一的时间。

    A 

    其他更多提高MATLAB运行速度的方法,参考

    易夕:如何提高MATLAB程序运行速度zhuanlan.zhihu.com
    易夕:MATLAB Tricks 专栏目录zhuanlan.zhihu.com
    展开全文
  • 为符合c语言的规范会用"\0"来表示结尾,且字符串的长度len不包含结尾标识符"\n"。 SDS优势 Coding的哔哔叨叨 1、 O(1)时间复杂复杂度获取字符串长度。sds内部维护这字符串长度的len变量,可以直接读取,时间复杂度...

    9ff314ec6f3d19ad3dba1fbfc056122f.png

    今天来聊个关于redis的话题,redis的数据结构,我之前的一篇文章,专门写过Redis的数据类型,及其简单的应用场景。今天这篇文章来专门聊一下Redis的数据结构,它的底层是如何存储的。

    Redis的string类型

    String概述

    Coding的哔哔叨叨

    1. String结构可以存储字符串或者各种类型的二进制数据。

    2. 能表达3种类型:字符串、整数和浮点数。根据场景相互间自动转型,并且根据需要选取底层的承载方式。

    3. value内部以int、sds(简单动态字符串sample dynamic string)作为结构存储,sds是可以动态修改的字符串,类似于java中的ArrayList,采取预分配冗余空间的方式来减少内存的频繁分配。int存放整型数据,sds存放字节/字符串和浮点型数据

    4. Redis 底层是用C语言编写的,可是在字符存储上,并未使用C原生的String类型,而是定义了自己的字符串结构 Simple Dynamic Stirng,简称SDS。

    SDS内部结构

    Coding的哔哔叨叨

    struct sdshdr {  int len; // 记录buf数组中已使用字节的数量,等于SDS所保存字符串的长度   int free; // 记录buf数组中未使用字节的数量   char buf[];// 字节数组,用于保存字符串 };
    • sds用buf数组存储字符串的内容,但数组的长度会大于所存储内容的长度。会有一格专门存放”\0”(C标准库)作为结尾,还有预留多几个空的(即free区域),当append字符串的长度小于free区域,则sds不会重新申请内存,直接使用free区域。

    • 为符合c语言的规范会用"\0"来表示结尾,且字符串的长度len不包含结尾标识符"\n"。

    e52da1618301bbee80282a7a5aa5598c.png

    SDS优势

    Coding的哔哔叨叨

    1、 O(1)时间复杂复杂度获取字符串长度。

    sds内部维护这字符串长度的len变量,可以直接读取,时间复杂度为O(1)。对于传统c的字符串需要遍历整个字符串,知道遇到结尾标识符"\0",时间复杂度为O(N)。

    2、缓冲区溢出规避

    所谓缓冲区溢出即所需要的内存超出了实际的内存。因此对于C字符串来说,要特别注意内存分配,回收使用问题。比如,向一个现有字符串内添加特定字符时,需要保证当前已经分配了这足够的内存。 

    与C不同的是,redis自定义字符串,使用预分配内存空间来规避缓冲区溢出发生

    当需要对SDS进行操作时,首先会检查自己当前free空间够不够,不够的话会进行扩容,内存检查相对于C变成了内部预置操作。关于扩容我们下文会进行详述,大家接着往下看。

    3、减少内存重分配次数

    在C中,对字符串进行操作前都需要进行内存分配,同时,操作完成后需要进行内存回收,一次操作至少涉及一次内存分配操作。

    大家应该都知道分配内存是一个复杂且精细的操作,耗时耗资源,针对此缺陷,Redis采用了空间预分配+惰性删除相结合来的策略

    • 空间预分配

    空间预分配用于优化Redis字符扩展操作。所谓预分配,就是说在一次扩展操作中,分配的内存空间会大于实际需要的内存空间。

    预分配空间的大小基于以下规则计算:

      • SDS len<1M:分配len长度空间作为预分配空间;
      • SDS len>=1M:分配1M空间作为预分配空间;

    这样在下次进行字符串操作的时候,如果需要的空间小于当前SDS free空间,就不在执行内存扩展重新分配操作。

    SDS的空间预分配机制,使得一次扩展操作所需要的内存重分配次数变为<=1。

    • 惰性删除

    所谓惰性删除,即调整删除SDS部分数据时,不会立刻执行内存回收操作,而是会保留空出来的内存,并更新free属性值,以备将来字符扩展需要时,可以直接使用。

    当然Redis也提供了主动释放未使用内存的方法。

    4、二进制安全

    C字符串由于特殊的编码要求只能保存文本数据。

    SDS相关的功能方法会以二进制的形式来操作SDS存储的数据,没有任何中间操作,存储最原始的数据,因此不会有字符层面的因素影响。

    SDS可以保存任何源的二进制数据,字符、图片、文件或者序列化的对象等等。

    String的最大长度

    Coding的哔哔叨叨

    Redis中String是最常见的数据类型,包括redis的key使用的数据结构都是这个string类型(redis的不同类型的数据结构,仅体现在value的结构不一样),而redis中规定了string的最大长度为512M。
    /** 检查容量大小的方法*/static int checkStringLength(client *c, long long size) {    // 超出了512M,就直接报错    if (size > 512*1024*1024) {        addReplyError(c,"string exceeds maximum allowed size (512MB)");        return C_ERR;    }    return C_OK;}

    Redis的存储:ReidsObject

    Coding的哔哔叨叨

    799e24dd0ff41679db67dbb0a4ca4dc8.png

    • redis对象内部存储形态。

    1. type:数据类型、例如sting、hash、list、set、zset等,值类型查看命令【type】。

    2. encoding值存储内部实现的数据结构。

    3. lru最后一次被访问时间,辅助回收,可以通过object idletime {key} 在不更新lru属性情况下查看key的空闲时间。

    4. refcount当前对象被引用次数,辅助回收,可以通过 object refcount {key} 查看引用数,当对象为整数且值在范围在[0-9999]时,redis可以通过共享对象的方式来节省内存。前共享对象池只对整数设置了0~9999个共享对象,一方面整数对象池复用率最大,同时等值判断上时间复杂度为O(1)。

    5. *ptr数据本身或者指向数据的指针,redis3.0之后,长度在39以内的字符串数据,内部编码为embstr,内存创建时,字符串和redisObject一起分配,减少一次内存分配。

    • value对象通常具有两个内存部分:redisObject部分和redisObject的*ptr指向的sds部分。创建value对象时,通常需要为redisObject和sds申请两次内存。但对于短小的字符串,可以把两者连续存放,所以可以一次性把两者的内存一起申请了。

    • Redis 的字符串共有两种存储方式,在长度特别短时,使用 emb 形式存储 (embedded),当长度超过 44 时,使用 raw 形式存储。

    • embstr 存储形式是这样一种存储形式,它将 RedisObject 对象头和 SDS 对象连续存在一起,使用 malloc 方法一次分配。而 raw 存储形式不一样,它需要两次 malloc,两个对象头在内存地址上一般是不连续的在字符串比较小时,SDS 对象头的大小是capacity+3——SDS结构体的内存大小至少是 3。意味着分配一个字符串的最小空间占用为 19 字节 (16+3)。如果总体超出了 64 字节,Redis 认为它是一个大字符串,不再使用 emdstr 形式存储,而该用 raw 形式。而64-19-结尾的\0,所以empstr只能容纳44字节。

    491e5e70c1f7209268a6bab2bc3a56d8.gif不积跬步,无以至千里。

    文章有帮助的话,点个转发、在看呗215acaac55aacd725d9376e4781ad631.gif

    谢谢支持哟 (*^__^*)

    END

    a98809eec60079de8ee08cab23b0fbba.gif?a98809eec60079de8ee08cab23b0fbba.gif

    7211af1e5ca86790b7a725a833220da8.png

    展开全文
  • C 语言没有提供名字空间(namespace)的概念(像在 C++ 或 Python 中那样的),因此通常使用命名约定进行模拟。使用名字空间的主要原因是避免名字碰撞(naming collisions )—— libwoot 和 libkool 都提供了叫做 ...

    41c70a055896fdd9e4eab5172ad1a7dc.png

    本文翻译自 Writing a C library part 3

    作者:davidz

    作者主页:https://www.blogger.com/profile/18166813552495508964

    文章源地址:http://davidz25.blogspot.com/2011/06/writing-c-library-part-3.html

    前篇:part one part two

    模块化和名字空间

    C 语言没有提供名字空间(namespace)的概念(像在 C++ 或 Python 中那样的),因此通常使用命名约定进行模拟。使用名字空间的主要原因是避免名字碰撞(naming collisions )—— libwoot 和 libkool 都提供了叫做 get_all_objects() 的函数,如果程序链接了这两个库,该使用哪一个函数呢?名字空间的使用是命名策略的重要组成部分,它用于变量名,函数名,类型名(包括 structs,unions,enums 和 typedefs)和宏。

    标准约定是使用短标识符,例如在 libnm-glib 中你会看到 nm_NM 的使用,在 Clutter 中是 clutterClutter,在 libpolkit-agent-1 中是 polkit_agentPolkitAgent。对于不对类型使用驼峰式大小写(CamelCase) 的库,一般对函数和类型使用相同的前缀 —— 例如,libudev 的前缀就是 udev

    不恰当使用名字空间的代码不但难以集成到其他的库和程序中(符号碰撞的概率很大),它也存在着与未来加入到 C 标准库或 POSIX 标准的函数发生碰撞的可能。

    在 C 中使用名字空间(讽刺的是,名字空间在语言中没有被很好的支持)的一个好处便是,仅仅通过查看源代码的片段就可以更容易确定代码在做什么 —— 例如,当你看到一个东西被加入到了一个容器中,你一般是不会对这是 GtkContainer 的 add 方法还是 ClutterContainer 的 add 方法感到疑问的,因为 C 的名字空间强制程序员的行为显而易见,不论是好是坏。

    除了选择良好的命名策略外,注意到库导出的符号的可见性是可以微调的。

    在命名这个话题上,避免使用 C++ 的关键字(例如 class)来命名变量通常是个好主意,至少在你期望在 C++ 中使用的头文件中加上 extern "C"。另外,一般避免使用 C 标准库 / POSIX 中的函数名作为变量,例如 "interface" 或 "index",因为这些函数可能被定义为宏。

    清单

    • 选择一个命名约定 —— 并坚持使用它
    • 不要导出非公共 API 的符号

    错误处理

    如果有一个充分描述了 C 语言错误处理的陈述,那人们和可能难以就其达成共识。然而,大多数程序员会同意这一点:错误可以被分为两类 1) 程序员错误(programmmer error);2) 运行时错误(run-time error)。

    程序员错误是指当程序员没有正确使用函数的错误 —— 将非 UTF-8 字符串传递给期望参数是合法 UTF-8 字符串的函数,例如 g_variant_new_string() (如果不确定的话,可以使用 g_utf8_validate() 在调用函数前对字符串进行验证),将不合法的 D-Bus 名字传递给 g_bus_own_name() (如果不确定,可以使用 g_dbus_is_name() 或 g_dbus_is_unique_name() 进行验证)。

    大多数库在使用不恰当的时候都会有未定义行为(undefined behavior) —— 在 GLib 中,宏 g_return_if_fail() / g_return_val_if_fail() 被用于对g_variant_new_string() 和 g_dbus_own_name() 的检查。另外,为了性能,在编译 GLib 自身或其他使用 GLib 的应用时,这些检查可以通过使用宏 G_DISABLE_CHECKS 来关闭(通常不用)。然而,检查不会覆盖所有的情况,因为检查的代价很昂贵。结合 G_DEBUG 标志,在 G_DEBUG=fatal-warnings 的环境中,可以更简单地捕获错误。

    使用 g_return_if_fail() 形式的检查通常是一种折衷(本可以不用的) —— 例如,GLib 一开始没有在 g_variant_new_string() 进行 UTF-8 的检查 —— 它只有在可观数量的用户将非 UTF-8 字符串传递给该函数,并造成了在不相关的代码中极难追踪的错误时,才被加入到函数中(可以在 commit message 看到更多细节)。 如果检查开销是不能接受的,程序员程序员可以使用 g_variant_new_from_data() 来将 TURE 作为信任参数传给函数。

    即便有一个进行了合理的参数验证的库(在早期发现程序员错误),如果你将垃圾值传递给函数,你通常最终会得到未定义行为,未定义行为可以是任何东西,比如将你的磁盘格式化,或蒸发掉半径五英里内的酒(evaporating all booze in a five-mile radius (oh noz))。这也就是为什么一些库简单地调用 abort() 而不是继续假装什么也没发生,一般而言,一个 C 库永远不能保证它不会在传递给它任意参数时发生爆炸 —— 例如用户可能传递指向无效数据的指针,然后就炸了,在库尝试访问它时,SIGSEGV 会被引发。当然,库也可以尝试恢复,通过使用 longjmp(3) ,但是因为它是一个库,它不能像信号处理程序那样用于处理进程间的状态。不幸的是,即便是聪明人,有时也会没有发现调用者的责任(fail to realize that the caller has a responsibility),而是选择指责库,而不是库的使用者。大多数情况下,像这样的问题只是通过将文档丢到问题上来进行解决(读文档)。

    总结一下,当谈到程序员错误时,一个要点便是:将函数接受的输入类型详细准确地记录在文档中通常是个好主意。俗话说:“相信是好的,但控制更好”("trust is good, control is better"),使用 g_return_if_fail() 样式的检查(可能提供不进行检查的 API),验证程序员是否正确也是一个好主意。另外,如果你的代码进行了相应的检查,确保使用了检查的函数是公有的,这样就有机会在调用函数前对输入进行验证(可见于:notes on errors in libdbus)。

    运行时错误,例如 fopen(3) 返回了 NULL(例如要打开的文件不存在或进程没有权力打开它),g_socket_client_connect() 返回 FALSE(可能没有足够的地址空间供 8GiB 的数组使用)。根据定义,运行时错误是可以恢复的,即便你使用的代码将某些错误(像是 malloc(3) 失败)看作不可恢复的,因为处理某些运行时错误(比如内存耗尽 (OOM))会使得 API 更复杂,不论是在函数水平(可能需要传递 错误(处理)参数),还是说需要在大多数数据类型上使用事务性语义(transactional semantics)比如回滚(rollback)。(这方面的说明可见于:write-up on why handling OOM is hard 和 good explanation of Linux's overcommit feature).

    对于简单的库,使用标准库的 errno(libc's errno) 是最简单的处理运行时错误的方法(因为它是线程安全的,而且每个 C 程序员都知道它)。但在同时也需要注意到,一些使用了 asprintf(3) 的函数没有将 errno 设置为 ENOMEM,比如在内存分配失败时。如果你的库是基于像是 GLib 的库的话,那就使用基库的错误类型,比如 GError ,作为运行时错误。在 cairo 2D graphics library 中使用了一种(is the one)有趣的错误处理的方式,对象实例对错误状态进行了追踪。(例子见于 cairo_status() and cairo_device_status())。还存在许多方法来追踪运行时错误 —— 一如既往,最重要的是在编写 C 库时保持一种一致性。

    清单

    • 将合法和不合法(如果有的话)的参数范围在文档中说明,并为程序员提供验证参数合法性的功能(除非实在是微不足道)。
    • 尝试在公共 API 边界对传入参数进行验证
    • 建立处理程序员错误的一种体系(例如,未定义行为或 about())
    • 建立处理运行时错误的一种体系(例如,使用 errno 或 GError)
    • 确保你所使用的处理运行时错误的方法能够映射到常见的异常处理(exception handling)系统

    封装和面向对象设计

    虽然 C 语言没有内置面向对象编程(object-oriented programming)支持,许都 C 程序员却是这么做的 —— 在很多方面,几乎很难不这么做。事实上,许多 C 程序员将 C 的简单性(和 C++ 相比)视作一个特性,这样你就不会与任何一个对象模型(object model)绑定。—— 例如,Linux 内核使用了various OO techniques,而 GLib/GTK+ 有它自己的动态类型系统,叫做 GType,GObject base class (许多类的派生源) 就是在它之上建立的。

    建立自己的对象模型当然得付出相应的代价 —— 它通常包括更多的类型(更长的标识符),和包括对 register properties(寄存器属性), add private instance data(添加私有实例数据)的更多函数调用,等等(比如这里的例子example )。另一方面,动态类型系统通常提供了一定水平的 type introspection (类型内省),因此可以通过使用 g_object_bind_property() 函数将 whether a check-button widget is active 与 whether an text-entry widget should use password mode 轻易连接起来。GObject 中的多态(Polymorphism)是由嵌入类结构中的虚函数表(virtual method table )提供的(example ),它也提供了一个使用函数指针的 C 函数(example ) —— 注意到派生类型可以访问这个类结构,直到派生链的链头(example )。

    C 中的面向对象设计的一个重要特性是,它通常通过使用不透明数据类型(opaque data types)达到封装(encapsulation)和数据隐藏的效果 —— 这是可取的,因为它允许对数据类型进行扩展(比如加上更多的属性(properties)和方法)而不需要对使用库的程序进行重新编译。在不透明数据类型中,在 C 结构中的字段会对用户表现为隐藏,并通过使用 getter (example)和/或 setter (example)变得可用 —— 另外,如果对象模型支持属性,成员也可以作为属性而可用(example ) —— 例如,这对于在属性发生变化时进行通知(notifying when the property changes)是有用的。

    当然,不是所有的数据结构都需要成为全面(full-blown)的 GObject —— 例如,某些情况下数据隐藏并不可取(有时使用 C getter 函数是很尴尬的)或在内部循环使用的速度太慢(直接数据访问无疑会更快)。另外,对于简单的数据结构,有时直接在代码中对其进行初始化更为可取。

    即便没有全面使用对象模型(像是 GType 和 GObject),使用非透明数据结构和 getters/setters 永远不是个坏注意。作为它的一个有趣的例外,注意到有些库显式允许扩展一个 C 结构而不需要考虑 ABI 的变化(allows extending a C structure without considering it an ABI change) —— 当没有简单的方法来确保这一点时(用户可能会把结构分配在栈上),库的作者至少能够总是告诉程序员不要这么做(也许有用吧)。

    清单

    • 为你的库建立一个对象模型(如果适用的话)
    • 尽可能地隐藏实现细节,在不影响性能的情况下
    • 确保你能在不破坏 API 和 ABI 的情况下对库进行扩展
    • 如果可能的话,在一个已有的且易于理解的类型系统上建立你的库(比如 GLib)
    include-yy:写个 C 库,part 1zhuanlan.zhihu.com
    1d820ff708cfa4679f97c6f13a41e15e.png
    include-yy:写个 C 库,part 2zhuanlan.zhihu.com
    0f8ab56bb0c84b69b4e7eae4de40eab2.png
    展开全文
  • 编程风格类规则的一个很好的例子是规则5.3“在内部作用域中声明的标识符不应掩盖在外部作用域中声明的标识符”。这显然是一种关于编码风格的警告。违反此规则本身不会导致程序崩溃,但是这样的声明会使代码更难于...
  • 宏定义是用一个标识符来表示一个字符串,在宏调用中将用该字符串代替宏名。给程序员提供了便利,使程序更加清晰,便于阅读和理解,进一步提高了程序的运行效率,对于嵌入式系统而言,为了能达到性能要求,宏是一种很...
  • 第一种:构造器参数循环依赖Spring容器会将每一个正在创建的Bean 标识符放在一个“当前创建Bean池”中,Bean标识符在创建过程中将一直保持在这个池中。因此如果在创建Bean过程中发现自己已经在“当...
  • C++语言:成功解决未定义标识符 "string"、未定义标识符 "cout"、“name”: 未知重写说明符 目录 解决问题 解决方法 解决问题 未定义标识符 "string"、未定义标识符 "cout"、“name”: 未知重写说明...
  • c++中总是出现未定义标识符的错误提示?1.代码总是出现未定义标识符的错误提示2.源码#include "stdafx.h"#include "vpn.h"#include "vpnDlg.h"#ifdef _DEBUG#define new DEBUG_NEW#endif// CvpnAppint BEGIN_MESSAGE...
  • C++未定义标识符ULONG,UINT的报错解决方法 VS2019 在使用VS 2019中 ulong,uint出现报错情况,如下图: 解决方法如下: 1、加入头文件#include <wtypes.h> 2、#define ulong ULONG 3、#define uint UINT #...
  • C++求助:未定义标识符“gets”

    千次阅读 2020-09-28 01:29:59
    问题表现 生成首字母缩写的代码里面的“gets”变成了未定义标识符,发个贴向大佬求助QAQ 有亿点点急
  • 来源链接:https://blog.csdn.net/FussyCat/article/details/88309197 问题表现 没怎么用过C++写过完整的项目,今天闲来无事,便创建个c++的工程玩玩,结果一个简单的打印输出就给卡住了,无法打开
  • 我在一个.cpp文件中已经声明该函数,见下图 [img=... 但是在执行主函数时,依然编译不过 ... ...我也试过在主函数前面加exter,但是编译依然有问题,函数中的返回值提示不正确。...
  • 踩了个C++未定义标识符"cout"的坑

    万次阅读 多人点赞 2019-03-07 15:17:14
    没怎么用过C++写过完整的项目,今天闲来无事,便创建个c++的工程玩玩,结果一个简单的打印输出就给卡住了,无法打开文件“iostream.h”,““cout” 未声明的标识符”,这是什么鬼。。。 首先第一个问题,无法...
  • 这个是版本导致的,在vs code中,找到.vscode文件夹下c_cpp_properties.json,将cppStandard的属性值改为c++11:
  • opencv3版本代码用opencv4版本资源会出现这种错误。 在opencv4是版本中,如果用老的opencv的代码,如 cvtColor(src, grayImg, CV_RGB2GRAY); 则会出现报错,是因为opencv更新后的版本问题 解决方法1: ...
  • CString未定义标识符

    千次阅读 2020-04-13 15:23:08
    创建了一个C++的动态链接库,属性上已经选中了dll中支持MFC,仍报错CString未定义标识符, 在工程的stafx.h中,增加了如下语句 #include <afxext.h> // MFC 扩展 问题解决了,CString并非是标准类型,为...
  • #pragma warning(default:4146) #import "C:\\Program Files\\Common Files\\System\\ado\\msado15.dll" no_namespace rename ("EOF","adoEOF") #pragma warning(default:4146) ... C++连接数据库总是报错
  • C++中的getline未定义标识符

    千次阅读 2019-05-08 16:16:03
    在网上查询资料说的是添加如下头文件 #include <stdlib.h> #include <stdio.h> 但是我的VS2015编译器中还是不正确,应该是添加头文件 #include <string> ...
  • ``` vector<int> filter2(const vector<int> &vec,int val,less<int> &...而且我发现,在第一段代码中,先出现的bind2nd会提示未定义标识符——不管这个bind2nd出现在while表达式以内还是以外。 求大神解答!!!谢谢

空空如也

空空如也

1 2 3 4 5 ... 9
收藏数 164
精华内容 65
关键字:

c++未定义标识符

c++ 订阅