通过各种方式发现的C/C++有意思的知识/性质/bug,以及一些零碎的算法

         我最近在读《Effective C++》以及做学校的课程设计,遇到了一些奇奇怪怪/很有意思的知识/性质/bug,同时,我有时候也会想起自己以前遇到的一些有意思的事情,再加上课上学到的一些算法,权且都写在这里。本文将会持续更新。

目录

1. local static 对象

2. C++成员初始化次序

3. C++为class默认生成的函数

4. 涉及指针的传参

5. 拓展欧几里得算法

6. C与C++的字符数组定义

7. C++结构体的大小

8. const 成员函数

9. C/C++中的const和define与数组长度

10. strcat的经典错误

11. strcpy的经典bug

12. 动态分配内存的经典bug

13. auto_ptr 的复制行为

14. 在编写socket通信时发现的关于sscanf的bug

参考文献


1. local static 对象

        在函数内定义一个static对象,函数结束后,该对象仍然存在,直至整个程序结束,但是由于函数结束了,所以这个变量无法被使用。 然而,如果我们重新调用函数的话,它就又复活了(或者说它根本没死,只不过是在可访问与不可访问之间反复横跳),测试如下:

#include<iostream>

void func(){
    static int a = 100;
    std::cout<< &a<<std::endl;
}

int main(){
    func();
    func();
}

         输出为:

0x403010
0x403010

        两个地址是一样的,说明两次函数调用里的变量是同一个。 

2. C++成员初始化次序

        base classes 早于其 derived classes 被初始化,class的成员变量总是以其声明次序被初始化。另外,C++对定义于不同编译单元内的non-local static 对象的初始化次序无明确定义,通过以下方法可以规避这一问题:将每个non-local static对象放到一个函数里,该对象在此函数里被声明为static,函数返回一个这个对象的引用,用户通过调用函数的方式使用这些对象。即:把non-local static 对象转为 local static对象。

3. C++为class默认生成的函数

        在类中没有相关函数的前提下,C++会默认生成一个default 构造函数,拷贝构造函数,拷贝赋值函数(重载’=‘号)以及析构函数。

4. 涉及指针的传参

        假定我们有一个指针int* p,一个int a。现在要通过函数 func1() 修改a的值,那么将a作为参数传给 func1() 是没有作用的,需要将p传给 func1() 。如果又有一个 func2() ,用来修改p的指向(也就是修改指针本身而不是修改指针指向的对象),那么传 p 是没有用的,需要传 &p,或者传一个**q,q指向的对象是p。相关测试如下:

#include<iostream>

void func1(int a){
    a = 100;
    std::cout<<"In func1: a = "<< a <<std::endl;
}

void func2(int *p){
    *p = 100;
    return;
}


void func3(int* p, int*& q){
    static int a = 200;
    //这里相当有意思,如果你的a是int而不是static int,会导致
    //程序结束之后p和q指向的地址的内容已经被销毁了
    //此时main函数里q的输出变成0而不是200,而p是不受影响的,因为传的不是p的引用
    //我一开始还觉得函数里的static int没什么用,结果这里写测试的时候用上了
    //其实我觉得遇到像q指到了被销毁的内容的情况就该直接报异常,终止程序
    //不然会导致一些令人迷惑的结果
    //但是程序运行结果自动补零了,我不认为是好事
    //最后还是要靠程序员自己来规避这个问题
    p = &a;
    q = &a;
    return;
}

int main(){
    int a = 1;
    int b = 2;
    int *p = &a;
    int *q = &b;
    std::cout<<"Original a = "<<a<<std::endl;// a=1
    func1(a);
    std::cout<<"In main, after func1, a = "<<a<<std::endl;// a=1
    func2(p);
    std::cout<<"In main, after func2, a = "<<a<<std::endl;// a=100
    std::cout<<"In main, before func3, *p = "<<(*p)<<std::endl;// *p=100
    std::cout<<"In main, before func3, *q = "<<(*q)<<std::endl;// *q=2
    func3(p,q);
    std::cout<<"In main, after func3, *p = "<<(*p)<<std::endl;// *p=100
    std::cout<<"In main, after func3, *q = "<<(*q)<<std::endl;// *q=200

}

        测试结果如下: 

Original a = 1
In func1: a = 100
In main, after func1, a = 1
In main, after func2, a = 100
In main, before func3, *p = 100
In main, before func3, *q = 2
In main, after func3, *p = 100
In main, after func3, *q = 200

        涉及到指针的传参导致的bug我曾经遇到过很多次,我为了de这些bug薅掉的头发数不胜数。这个问题对现在的我来说应该是不会造成什么困扰了,但是希望新人们看到涉及到指针的传参时能多留个心眼。

5. 拓展欧几里得算法

        在欧几里得算法的基础上加了一点拓展,使得其输入为 a,b 返回值为 x,y,d,令 d=gcd(a,b),且 a*x + b*y = d,代码如下:

#include<iostream>
#include<vector>
using namespace std;

vector<int> E_E(int a, int b){
    vector<int> ans;
    if(b == 0){
        ans.push_back(1);
        ans.push_back(0);
        ans.push_back(a);
        return ans;
    }

    vector<int> tmp = E_E(b,a%b);
    ans.push_back(tmp[1]);
    int second = tmp[0] - (a/b)*tmp[1];
    ans.push_back(second);
    ans.push_back(tmp[2]);
    return ans;
}


int main(){
    int a;
    int b;
    for(int i=0;i<5;i++){
        cin>>a>>b;
        vector<int> tmp = E_E(a,b);
        for(int j=0;j<3;j++)
            cout<<tmp[j]<<" ";
        cout<<endl;
    }
    return 0;
}
6. C与C++的字符数组定义

        以下代码:

char* a = "NULL";

        在C++中会报warning:

warning: ISO C++ forbids converting a string constant to 'char*' [-Wwrite-strings]

        而在C中是被允许的,不会报任何错误。

        我在用C写课设的时候想试验一下,发现C和C++在这里有差异。

7. C++结构体的大小

        结构体占用的内存是连续分配的。记结构体中最大变量的大小为m bytes,那么结构体的每一个变量的大小都会被扩充为 m bytes,也就是说,结构体的大小为 m 的整数倍。但是如果单独输出结构体中某个变量的大小,则又不是m bytes,而是其本身的大小。

        另外,C++似乎只是会改变单个变量的大小,并不会改变数组、字符串中每个单位的大小。文字描述不清楚,建议看代码:

        

#include<iostream>
using namespace std;
struct test{
    int a = 1;
    char b = 'a';
    char* c = "abcdefgh";
};
int main(){
    test t;
    char c1 = 'a';
    cout<<sizeof(t)<<endl;
    cout<<sizeof(t.a)<<endl;
    cout<<sizeof(t.b)<<endl;
    cout<<sizeof(t.c)<<endl;
    cout<<sizeof(c1)<<endl;
}

           测试结果如下:

16
4
1
8
1

        第一个输出说明结构体大小是16byte,明显是4+4+8的组合,说明 char b 的大小被扩充为4 bytes了,但如果单独输出b的大小,则会输出 1 byte,跟输出 char c1的结果一样。而且,c 的大小为8,说明c中每一个字符的大小都是1 byte,并没有被扩充。

        这个性质应该不只是C++的,在学习计组的时候有这个知识点,计算机会扩充结构体之中变量的大小。

        另外就是,为什么 sizeof 的输出会发生改变(4+1+8 和 16),这一点我还没有探明,个人猜测是sizeof 这个函数的问题,如果我找到了答案,会在下面补充。倘若有读者知道其中原因,我相当欢迎您在评论区或者私信指导。

8. const 成员函数

        const修饰的成员函数不能修改类中的变量。但是我们可以通过mutable修饰该变量来使其可被修改。代码如下:

class test{
    mutable int a;
    int b;
    void func() const{
        this -> a = 1;//可以,a被mutable修饰了。
        this -> b = 2;//报错,这个动作是非法的。
                      //报错信息为:“表达式必须是可修改的左值。”
    };
};

        查阅了网上的资料,原因是const修饰成员函数,使得函数中隐含的参数 test* this 变成了 const test* const this,这样一来,指针的指向以及指针所指对象的值都是不可改变的。

        这是别的博主的文章:c++基础(十二)——const修饰成员函数-CSDN博客icon-default.png?t=N7T8https://blog.csdn.net/qq_52302919/article/details/127129326

9. C/C++中的const和define与数组长度

        无论是在C还是C++中,define定义的数都可以作为数组的长度。在C中,const定义的量不可以作为数组的长度(const代表变量只读而不是常量),如果你尝试用const作为数组长度,会报一个错误“variably modified ‘xxxx(数组的名字)’ at file scope”。然而,C中enum可以作为数组的长度。在C++中,const修饰的数以及enum都可以作为数组的长度使用。

        根据《Effective C++》,使用define可能会导致一些不容易发现的错误,所以建议尽量使用const以及enum定义数组的长度。

10. strcat的经典错误

        strcat(str1,str2)用来把 str2 拼接到 str1 上。这个函数把 str2 的第一个字符放在原 str1 的 '\0' 的位置上(效果等同于删掉原 str1 的 '\0' 然后把 str2 拼接上去),同时会自动在新的 str1 的结尾添加 '\0'。关于 strcat 有一个很经典的错误,我在写课设的时候犯了。

        错误代码如下:

char str1[7] = "Hello \0";
char str2[6] = "world\0";
strcat(str1,str2);

        strcat 最阴间的地方在于 str1 必须为 str2 预留出足够的空间(而不是动态地扩充 str1 的空间),如果空间不足,则会报错,错误为 "stack overflow" 。

        正确代码如下:

char str1[100] = "Hello \0";
char str2[6] = "world\0";
strcat(str1,str2);

        str1 为 str2 留足了空间,就不会有问题了。这里也侧面说明了C字符串手动补 '\0' 的重要性,我不清楚C里面如果不补 '\0',开出这种远比内容大的空间会发生什么,可能正常的内容后面会自动补全随机的字符也说不定,但是手动补上了结束符之后我的课设代码运行得很完美(乐)。

        吐槽一下 C 字符串,从方便的角度来说真是不如 C++ 。(非引战,不喜勿喷)

11. strcpy的经典bug

        以下代码是经典的strcpy的bug,会出现segment fault:

char* a = "ABCDE";
char* b;
strcpy(b,a);

        正确的代码如下:

char* a = "ABCDE";
char b[100] = "\0";
strcpy(b,a);

        也就是说你必须显式地为 strcpy 的 destination 字符串分配足够的空间,否则会导致段错误。

        C风格字符串真是......一言难尽。

12. 动态分配内存的经典bug

        通过类似以下代码的方式进行数组的初始化,会导致segmentation fault:

int* func(int size){
    int* tmp = (int)malloc(size*sizeof(int));
    return tmp;
}

int main(int argc, char* argv[]){
    int* a1 = func(10);
    a1[0] = 1;
}

        原因在于 tmp 确实被返回了,然而 tmp 所指向的内存在函数结束时被回收了,导致 tmp 指向了一片用不了的内存。把 tmp 赋值给 a1 毫无意义。这一点无论是 C 中的 malloc 还是 C++ 中的 new 都是一样的。

13. auto_ptr 的复制行为

        auto_ptr 是一个智能指针类,它有一个约束:“受auto_ptr管理的资源只允许至多一个auto_ptr指向它”,于是乎以下代码会导致注释中的结果:

auto_ptr<class_a> class_a_1(create_class()); //class_a_1智能指针被指向create_class创造出的class_a对象

auto_ptr<class_a> class_a_2(class_a_1); //class_a_2指向class_a_1指向的对象,随后class_a_1被赋值为NULL

class_a_1 = class_a_2; //class_a_1指向class_a_2指向的对象,随后class_a_2被赋值为NULL

        这个性质将导致auto_ptr的使用极其受限而且很可能会产生bug。

14. 在编写socket通信时发现的关于sscanf的bug
char default_message[1025];
memset(default_message, 0, 1025);
sscanf(default_message,"You are served by a server now. The public key is e:%lld, N:%lld.\n",&key[0],&key[2]);

int send_ = send(client_sock, default_message, strlen(default_message) , 0);

if(send_==-1){
    perror("Failed to send.\n");
    exit(-1);
}

        这里的 default_message 发出去是空的,send_ 的返回值是大于等于零的,并没有发生perror.换成下面就行了:

std::string default_message = "You are served by a server now. The public key is e:";
        default_message += std::to_string(key[0]);
        default_message += ", ";
        default_message += "N:";
        default_message += std::to_string(key[2]);
        default_message += "\n";

        int send_ = send(client_sock, default_message.c_str(), default_message.length() , 0);

        if(send_==-1){
            perror("Failed to send.\n");
            exit(-1);
        }

        目前还在探索原因。

参考文献

1.《Effective C++ 第三版》[美]  Scott Mayers 著

2. 《算法概论》 [美] Sanjoy Dasgupta 等 著

  • 4
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值