The Uncertainty Of C/C++
在學習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 main,void main是某些廠商的特定實現,不是所有編譯器都支援,在gcc上使用直接報錯誤。
或者深究一下,這就涉及到編譯後執行的啟動程式碼,這不是這個問題的重點,以後再分析。
4.為什麼“聯通”如此“悲催”
談到資料編碼時,同學說了一件有意思的事,開啟新建txt檔案,輸入“聯通”,然後儲存,再開啟,發現亂碼了,聯通躺槍了,原來這和“聯”這個字有很大關係,至於到底是什麼原因,大家可以自己搜尋下,文章多得是,希望這個小事可以適當為大家補充點關於編碼的知識。
5.字元指標和字元陣列初始化的差別
char a[] = "string literal";
char *p= "string literal";
這兩個初始化有什麼差別?差別是什麼?對於a[]、"string literal"、p他們儲存在什麼地方?如果我令p[1]='x';會出現什麼情況?好好思考下,真的很有意思,同時可以看下這篇文章對你理解這個問題有很大幫助,linkhttp://blog.csdn.net/xluren/article/details/8150723 如果對於p[1]='x';看完後還不明白的話,那就繼續多看幾遍。
6.當sizeof遇到struct
在平時程式設計或者在筆試面試中,我們經常會碰到sizeof求解儲存空間的問題,而這裡面最容易出錯的就是sizeof和struct的結合,詳細的問題及原理請參見http://blog.csdn.net/xluren/article/details/8151285
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.一個詭異的函式
13 struct in_addr t1,t2;
14 t1.s_addr= inet_addr("10.0.0.0");
15 t2.s_addr= inet_addr("10.0.100.100");
16 printf("%d----%d\n",inet_ntoa(t1),inet_ntoa(t2));
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.建構函式初始化列表中的成員初始化次序與他們在類中的宣告次序相同,與在初始化列表中的次序無關。
[[email protected] construct_row]$ less main.cc
#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;
}
結果:
[[email protected] construct_row]$ ./run
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.深拷貝和淺拷貝
淺拷貝就如同引用型別,而深拷貝就如同值型別。
淺拷貝是指源物件與拷貝物件共用一份實體,僅僅是引用的變數不同(名稱不同)。對其中任何一個物件的改動都會影響另外一個物件。舉個例子,有個蛋糕小明自己在吃,後來小強來了,小明很大方,好吧,咱倆共享這一個蛋糕,在吃的過程中,無論誰吃一口都會影響到對方少吃一口。
深拷貝是指源物件與拷貝物件互相獨立,其中任何一個物件的改動都不會對另外一個物件造成影響。同樣是吃蛋糕,小強來了,小明非常的大方,又給小強拿了一個新的蛋糕,兩個人互不影響,各持各的。
[[email protected] construct]$ less main_deep.cc
#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 編譯器)
[[email protected] construct]$ ./main
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.過載運算子有幾種情況
兩種,一種過載為類的成員函式,另一種為類的友元函式。