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