The Uncertainty Of C/C++

原文地址:http://blog.csdn.net/xluren/article/details/8145944

 

在学习c/c++语言中总有一些隐晦的地方让我们感觉到不确定,但知道其中的奥妙后,又会惊叹“啊,太巧妙了”,抑或对于语言的使用,已经达到了熟练或者甚至炉火纯青的地步,但是一刨根问底追究其本质的时候,我们却又不知所以然。慢慢地从新深入地再认真学习一遍,我也无法保证自己分析或查到资料百分之百正确,如果有什么错误之处还请留下宝贵意见。对于某一个不是非常清楚地,如果借用了您的部分blog,那么先在此谢过了。本篇文章会不断的更新~~~

注:以下所有的问题默认是c/c++,32位系统

一开始,我就不注意理论概念,认为只要我代码写的好(其实写的也不好),理论算什么,在这方面吃了很大亏,编码再好到头来也许还是做应用,希望藉此鞭策自己多了解理论的知识,知其然,还要知其所以然,如果你碰到类似的问题,可以告诉我,谢谢了~~~


1.声明和定义


什么是定义:定义就是(编译器)创建一个对象,为这个对象分配一块内存并给它取上一个名字,这个名字就是我们经常所说的变量名或对象名。但注意,这个名字一旦和这块内存匹配起来,它们就永远不会分开。并且这块内存的位置也不能被改变。一个变量或对象在一定的区域内(比如函数内,全局等)只能被定义一次,如果定义多次,编译器会提示你重复定义同一个变量或对象。
什么是声明:有两重含义,如下:
第一重含义:告诉编译器,这个名字已经匹配到一块内存上了,下面的代码用到变量或对象是在别的地方定义的,内存空间也是别处分配的。声明可以出现多次。
第二重含义:告诉编译器,我这个名字我先预定了,别的地方再也不能用它来作为变量名或对象名。比如你在图书馆自习室的某个座位上放了一本书,表明这个座位已经有人预订,别人再也不允许使用这个座位。其实这个时候你本人并没有坐在这个座位上。这种声明最典型的例子就是函数参数的声明,例如:void fun(int i, char c);
另外变量的声明一般通过关键字“extern”来实现的。所以,extern int i;int i;很容易分清楚哪个是定义,哪个是声明。 

2.数据定义的意义

1.定义了数据占用的内存空间大小;
2.定义了数据的取值范围;
3.定义了数据在内存中的存储格式;
4.决定了数据的运算规则;
5.为编译器提供了检查依据。 

3.如何书写main函数?

void main(),我想这种写法非常多,特别是刚学c语言的同学,特别是借助谭老师那本书的同学,其实这是错的,正确的解法有以下几种

int main(), int main(void) 或者int main(int argc, char *argv[]) (显然argc 和argv 的拼写可以随便),

标准没有void mainvoid main是某些厂商的特定实现,不是所有编译器都支持,在gcc上使用直接报错误。

或者深究一下,这就涉及到编译后运行的启动代码,这不是这个问题的重点,以后再分析。

4.为什么“联通”如此“悲催”

谈到数据编码时,同学说了一件有意思的事,打开新建txt文件,输入“联通”,然后保存,再打开,发现乱码了,联通躺枪了,原来这和“联”这个字有很大关系,至于到底是什么原因,大家可以自己搜索下,文章多得是,希望这个小事可以适当为大家补充点关于编码的知识。

5.字符指针和字符数组初始化的差别

char a[] = "string literal";

 char *p= "string literal";

这两个初始化有什么差别?差别是什么?对于a[]、"string literal"、p他们存储在什么地方?如果我令p[1]='x';会出现什么情况?好好思考下,真的很有意思,同时可以看下这篇文章对你理解这个问题有很大帮助,http://blog.csdn.net/wuyuan2011woaini/article/details/9409057 如果对于p[1]='x';看完后还不明白的话,那就继续多看几遍。

6.当sizeof遇到struct

在平时编程或者在笔试面试中,我们经常会碰到sizeof求解存储空间的问题,而这里面最容易出错的就是sizeof和struct的结合,详细的问题及原理请参见http://blog.csdn.net/wuyuan2011woaini/article/details/9409683

7.当sizeof遇到extern

假设在一个工程下有这么几个文件,一个head.c,一个main.c,还有一个head.h,head.h 如果遇到这种情况

          head.c                                       main.c

    extern int aaa[];                        int aaa[]={1,2,3,4};

    int size=sizeof(aaa);             

编译过程中会出现错误,为什么?我的理解是这样的: 

1.首先,程序的编译,在gcc下,我们会写

gcc -c  head.c      会产生 head.o,

gcc -c  main.c     会产生 main.o,

gcc -o  head.o main.o main.out,生成可执行程序(强烈建议摆脱IDE,尽量使用gcc,可以了解很多)

2.也就是说在编译第一句时,他会报错,这就又回到extern这个关键字了,他只负责声明,但是不分配内存,无法确定其大小,

所以使用sizeof必然会报错。

那好的,如果我声明的不是数组而是单纯的变量呢?结果是可以的,那又是为什么呢?想了解更详细的知识,可以自己好好看一下sizeof的实现和原理。

8.printf和cout在一起的时候

同学在实验某个越界问题时,用cout和printf输出的结果完全不同,心里好奇这两个函数的实现是怎么回事,查了一些资料,其中发现了一道很有意思的题

#include <iostream.h>
#include <stdio.h>
main()
{
	int a;
	for(a=0;a<24;a++)
	{
		printf("++++++++++++\n");
		cout<<"============\n"<<a;
        printf("############\n");
	}
    printf("@@@@@@@@@@@\n");
}


结果是什么?

#include <iostream.h>
#include <stdio.h>
main()
{
	int a;
	for(a=0;a<1024;a++)
	{
		printf("++++++++++++\n");
		cout<<"============\n"<<a;
        printf("############\n");
	}
    printf("@@@@@@@@@@@\n");
}


结果又是什么呢?


9.c/c++下不同的sizeof

printf("%d",sizeof('A'));

cout<<sizeof('A');

结果如何?原因在于c语言中会将'A'提升当做int去处理,而c++中则不会做这种处理,这也就决定了两者的结果不同

10.一个诡异的函数

struct in_addr t1,t2;
t1.s_addr= inet_addr("10.0.0.0");
t2.s_addr= inet_addr("10.0.100.100");
printf("%d----%d\n",inet_ntoa(t1),inet_ntoa(t2));


这是函数返回值的一种特殊情况,具体的解释看这里,http://blog.csdn.net/wuyuan2011woaini/article/details/9409763

11.printf传递参数的顺序

#include<stdio.h>  
   
int main(void)  
{  
    int a = 10, b = 20, c = 30;  
   
    printf("\n %d..%d..%d \n", a+b+c, (b = b*2), (c = c*2));  
   
    return 0;  
} 结果是什么?


12.构造函数初始化列表中的成员初始化次序与他们在类中的声明次序相同,与在初始化列表中的次序无关。

 
#include<iostream>
using namespace std;
class test
{
        public:
                test(int _i,int _m):i(_i),j(i),n(m),m(_m){};
                void display()
                {
                        cout<<i<<"\t"<<j<<"\t"<<m<<"\t"<<n<<endl;
                }
        private:
                int j,i;
                int m,n;
};
int main()
{
        test a(6,7);
        a.display();
        return 0;
}
   


结果:

6       -1080783512     7       7


13.构造函数初始化列表要先于构造函数中的语句的执行

14.拷贝构造函数的原型,使用场合

mystring(const mystring &otherstring)
{
	ptr=new char[otherstring.length+1];
	length=otherstring.length;
	strcpy(ptr,otherstring.ptr);
}


1.一个对象作为函数参数,以值传递的方式传入函数体;
2.一个对象作为函数返回值,以值传递的方式从函数返回;
3.一个对象用于给另外一个对象进行初始化(常称为复制初始化);

如果在前两种情况不使用拷贝构造函数的时候,就会导致一个指针指向已经被删除的内存空间。
对于第三种情况来说,初始化和赋值的不同含义是拷贝构造函数调用的原因。
如果在前两种情况不使用拷贝构造函数的时候,就会导致一个指针指向已经被删除的内存空间。对于第三种情况来说,初始化和赋值的不同含义是拷贝构造函数调用的原因。事实上,拷贝构造函数是由普通构造函数和赋值操作符共同实现的事实上,拷贝构造函数是由普通构造函数和赋值操作符共同实现的。

情景1代码

#include<iostream>
using namespace std;
class stu
{
        char *str;
        public :
                stu(char *_str)
                {
                        str=new char(strlen(_str));
                        strcpy(str,_str);
                }
                ~stu()
                {
                        delete []str;
                }
};
void print(stu a)
{
        cout<<"hello"<<endl;
}
int main()
{
        stu b("123");
        print(b);
        return 1;
}


情景2代码

#include<iostream>
using namespace std;
class stu
{
	char *str;
public :
	stu()
	{
		str=new char[11];
		strcpy(str,"hello");
	}
	stu(char *_str)
	{
		str=new char[strlen(_str)+1];
		strcpy(str,_str);
	}
	stu &operator=(const stu &otherstu)
	{
		if(this==&otherstu)
			return *this;
		delete []str;
		str=new char[strlen(otherstu.str)+1];
		strcpy(str,otherstu.str);
		return *this;
	}
	~stu()
	{
	      delete []str;
	}
};
stu print()
{
	cout<<"hello"<<endl;
	stu a("nice");
	return a;
}
int main()
{
	stu b;
	b=print();
	return 1;
}


情景3代码

#include<iostream>
using namespace std;
class stu
{
        char *str;
        public :
                stu(char *_str)
                {
                        str=new char(strlen(_str));
                        strcpy(str,_str);
                }
                ~stu()
                {
                        delete []str;
                }
};
int main()
{
        stu b("123");
        stu c=b;
        return 1;
}


15.深拷贝和浅拷贝

浅拷贝就如同引用类型,而深拷贝就如同值类型。

浅拷贝是指源对象与拷贝对象共用一份实体,仅仅是引用的变量不同(名称不同)。对其中任何一个对象的改动都会影响另外一个对象。举个例子,有个蛋糕小明自己在吃,后来小强来了,小明很大方,好吧,咱俩共享这一个蛋糕,在吃的过程中,无论谁吃一口都会影响到对方少吃一口。

深拷贝是指源对象与拷贝对象互相独立,其中任何一个对象的改动都不会对另外一个对象造成影响。同样是吃蛋糕,小强来了,小明非常的大方,又给小强拿了一个新的蛋糕,两个人互不影响,各持各的。

#include<iostream>
using namespace std;
class test
{
        public:
//              test(test &othertest)
//              {
//                      str=new char[strlen(othertest.str)+1];
//                      strcpy(str,othertest.str);
//              }
                test(char *_str="hello")
                {
                        str=new char[strlen(_str)+1];
                        strcpy(str,_str);
                }
                ~test()
                {
                        delete []str;
                }
                void print()
                {
                        cout<<str<<endl;
                }
        private:
                char *str;
};
int main()
{
        test a("hello fy");
        a.print();
        test b=a;
        b.print();
        return 0;
}


如果程序员不显示的定义拷贝构造函数,编译器会自动生成一个默认的拷贝构造函数,当然默认的是浅拷贝构造函数,注释掉的是深拷贝函数,没有那个显式的拷贝构造函数,有可能会出现指针悬挂,程序崩溃。特别是对象含所有指针成员时,浅拷贝两者共享一份资源,在析构释放时,会被重复释放两次,这样是不对滴,当然会出现指针悬挂,程序崩溃,崩溃的情况如下所示(gcc 编译器)

hello fy
hello fy
*** glibc detected *** ./main: double free or corruption (fasttop): 0x08ef3008 ***
======= Backtrace: =========
/lib/libc.so.6[0x2e1035]
/lib/libc.so.6(cfree+0x59)[0x2e1479]
/usr/lib/libstdc++.so.6(_ZdlPv+0x21)[0x4aa05c1]
/usr/lib/libstdc++.so.6(_ZdaPv+0x1d)[0x4aa061d]
./main(__gxx_personality_v0+0x2ac)[0x80488f4]
./main(__gxx_personality_v0+0x1ca)[0x8048812]
/lib/libc.so.6(__libc_start_main+0xdc)[0x28ee9c]
./main(__gxx_personality_v0+0x49)[0x8048691]
======= Memory map: ========
00247000-00252000 r-xp 00000000 08:02 769797     /lib/libgcc_s-4.1.2-20080825.so.1
00252000-00253000 rwxp 0000a000 08:02 769797     /lib/libgcc_s-4.1.2-20080825.so.1
0025a000-00275000 r-xp 00000000 08:02 769794     /lib/ld-2.5.so
00275000-00276000 r-xp 0001a000 08:02 769794     /lib/ld-2.5.so
00276000-00277000 rwxp 0001b000 08:02 769794     /lib/ld-2.5.so
00279000-003cb000 r-xp 00000000 08:02 769795     /lib/libc-2.5.so
003cb000-003cc000 --xp 00152000 08:02 769795     /lib/libc-2.5.so
003cc000-003ce000 r-xp 00152000 08:02 769795     /lib/libc-2.5.so
003ce000-003cf000 rwxp 00154000 08:02 769795     /lib/libc-2.5.so
003cf000-003d2000 rwxp 003cf000 00:00 0 
003db000-00402000 r-xp 00000000 08:02 769796     /lib/libm-2.5.so
00402000-00403000 r-xp 00026000 08:02 769796     /lib/libm-2.5.so
00403000-00404000 rwxp 00027000 08:02 769796     /lib/libm-2.5.so
00dd7000-00dd8000 r-xp 00dd7000 00:00 0          [vdso]
049ed000-04acd000 r-xp 00000000 08:02 326959     /usr/lib/libstdc++.so.6.0.8
04acd000-04ad1000 r-xp 000df000 08:02 326959     /usr/lib/libstdc++.so.6.0.8
04ad1000-04ad2000 rwxp 000e3000 08:02 326959     /usr/lib/libstdc++.so.6.0.8
04ad2000-04ad8000 rwxp 04ad2000 00:00 0 
08048000-08049000 r-xp 00000000 08:05 293936     /home/fy/cpp/construct/main
08049000-0804a000 rw-p 00000000 08:05 293936     /home/fy/cpp/construct/main
08ef3000-08f14000 rw-p 08ef3000 00:00 0          [heap]
b7f64000-b7f66000 rw-p b7f64000 00:00 0 
b7f6f000-b7f70000 rw-p b7f6f000 00:00 0 
bfb5a000-bfb6f000 rw-p bffe9000 00:00 0          [stack]
Aborted


16.c++赋值运算符重载返回类型是引用

因为赋值操作会改变左值,而 + 之类的运算符不会改变操作数,所以说赋值运算符重载要返回引用以用于类似 (a=b)=c 这样的再次对a=b进行写操作的表达式。+ 返回一个临时对象是合情合理的 ,你若返回引用大多数情况下也不会出错或导致某个操作数被意外修改,但这就使(a+b)=c这样的表达式可以出现,这就有点不符合约定了,当然,你也可以让 + 返回一个常引用。

具体形式如下:mystring & operator=(mystring &youstring); mystring +operator(mystring &youstring);

17.重载运算符有几种情况

两种,一种重载为类的成员函数,另一种为类的友元函数。

你可能感兴趣的:(The Uncertainty Of C/C++)