C语言学习第017课——C语言提高(一)
typedef的用法
1、定義指針類型
定義兩個char*的變量,p1和p2,使用C++代碼打印一下他倆的類型:
#include <iostream> #include<typeinfo>using namespace std;int test(){char* p1,p2;cout<< typeid(p1).name() <<endl;cout<< typeid(p2).name() <<endl; } int main() {test();return 0; }運行結果:
這是為什么呢?
定義指針還有一種寫法:
所以也就可以理解為,char修飾的是p1和p2, “*” 修飾的才是指針
如果想讓兩個打印結果都是Pc的話,應該這樣定義:
或者直接用typedef將char* 起一個別名,原理和上面代碼一樣的
#include <iostream> #include<typeinfo>using namespace std;typedef char* PCHAR;int test(){PCHAR p1,p2;cout<< typeid(p1).name() <<endl;cout<< typeid(p2).name() <<endl; }2、定義特殊類型
例如:在一個平臺上編譯代碼,需要用到long long 類型,而且用到不少這樣的類型,但是在其他平臺編譯代碼,庫里面不支持這個long long類型,只支持int類型,這個時候只需要使用typedef給long long類型起一個別名
typedef long long MYLONG;在之后需要定義long long類型的數據的時候,直接使用MYLONG定義
如果需要在另一個平臺編譯,只需修改:
這一行就OK了
void數據類型的用法
void字面意思是無類型,void* 無類型指針,無類型指針可以指向任何類型的數據,
void定義變量是沒有任何意義的,當你定義void a,編譯器會報錯,因為編譯器不知道分配多少內存給變量。
void真正用在以下兩個方面:
- 對函數返回的限定
- 對函數參數的限定
編譯器不知道分配多少內存的情況
上面說到一個使用void定義變量,編譯器報錯,以為不知道分配多少內存
還有一種情況
這種嵌套本身的結構體,編譯器也不知道分配多少內存,因為如果一直往里循環的話,人自己都算不出來要分配多少內存。
有符號數和無符號數的運算和轉換
首先說一下,有符號數和無符號數從表面上來看,是聲明的時候 一個有unsigned一個沒有,但是歸根結底是從二進制來看的,
在內存中,所有的數都是以反碼的形式存放的。
例如:
所以在運算過程中,要從二進制的層面上來考慮
1、int和unsigned int 進行(邏輯)運算
#include <iostream> using namespace std;int test(){int a = -1;unsigned int b = 16;if(a>b){cout<<"負數大于正數了!"<<endl;}return 0; } int main() {test();return 0; }運行結果:
知識點:有符號數和無符號數進行運算的時候,首先要將有符號數換算成無符號數,運算結果也是無符號數。
也就是說,-1最前面的符號位不按符號算了,算成數字的一部分
所以 很明顯,兩個都算成無符號數的話,-1比16大多了,所以會有上面的計算結果
練習:
分析:a+b 有符號數和無符號數進行運算,先換成無符號數
a=-1 原碼:10000000 00000000 00000000 00000001反碼:11111111 11111111 11111111 11111110補碼:11111111 11111111 11111111 11111111 unsigned int b = 16補碼:00000000 00000000 00000000 00010000 相加結果 00000000 00000000 00000000 00001111 = 15 ---------------------------------------------------------- c = -16 原碼:10000000 00000000 00000000 00010000反碼:11111111 11111111 11111111 11101111補碼:11111111 11111111 11111111 11110000 unsigned int d = 15補碼:00000000 00000000 00000000 00001111 c+d 11111111 11111111 11111111 11111111 = 4294967295(無符號數)2、unsigned char和char運算
#include <iostream> using namespace std;int test(){char a = -16;unsigned char b = 14;if(a>b){cout<<"負數大于正數了!"<<endl; //沒打印}cout<<a+b<<endl; //-2return 0; } int main() {test();return 0; }這個運行結果出乎意料,是因為什么呢?
知識點:C語言中比int小的整型(包括short 、unsigned short 、 unsigned char和char)在運算中都要轉換成int然后進行運算,至于為什么選擇轉換為int,應該是從效率上考慮的,因為通常情況下int的長度被定義為機器處理效率最高的長度,比如32位機上,一次處理4個字節效率是最高的,所以雖然short更節省內存,但是在運算中的效率,是int更高。所以上面,無論是邏輯運算a>b還是算術運算a+b中a和b都默認轉換成了int,不是unsigned int!轉換成了int,不是unsigned int!轉換成了int 不是unsigned int!
如果是unsigned的類型轉換成int類型,高位補0.
如果是signed的類型轉換成int類型,如果原來最高位是1則補1,如果是0則補0。
那么上面的結果就解釋的通了:
練習:
#include <iostream> using namespace std;int test(){char a = -16;unsigned char b = 255;char c = 255;cout<<a+b<<endl;//239cout<<a+c<<endl;//-17return 0; } int main() {test();return 0; }分析:
char a = -16 換成int類型 11111111 11111111 11111111 11110000 unsigned char b = 255補碼: 11111111 換成int類型(是unsigned類型,所以前面直接補0)00000000 00000000 00000000 11111111 char c = 255補碼: 11111111 換成int類型(是signed類型,最高位是1,所以前面直接補1)11111111 11111111 11111111 11111111 ------------------------------------------------------- a+b 00000000 00000000 00000000 11101111 =239 a+c 11111111 11111111 11111111 11101111 負數取反: 10000000 00000000 00000000 00010000+1: 10000000 00000000 00000000 00010001 = -17局部定義的變量不能當做返回值
#include<stdio.h> #include<stdlib.h> #include<string.h>char* getStr(){char str[] = "hello world!";return str; } int main(void){char* s = NULL;s = getStr();printf("s = %s\n",s); }以上代碼,一個函數里面定義了一個字符串,然后直接返回了,在主函數中取到這串字符串,然后打印,很簡單,但是運行結果是s=(null),為什么呢?
因為在函數中定義的字符串是保存在棧中的,函數一結束,棧中的內容就被回收了,在main函數中就無法打印出hello world了,
有人說字符串名稱str從一定意義上來說他是一個指針,這樣返回指針也不行嗎?
那我們來說一下這幾行代碼的總體流程:
首先以
這種方式定義的字符串,hello world 是存儲在常量區的,這一行代碼的意思是,從常量區中將“hello world”復制到棧區,然后復制過來的第一個位置的指針賦值給str,函數中返回的str,其實返回的就是這個指針,函數結束,意味著復制過來的hello world被回收,而str對應的指針,也僅僅就是一個地址了,里面的內容已經不存在了。所以打印的時候會出現NULL。
指針的間接賦值
有了上面的例子,來看一下下面的代碼,看一下打印結果是什么
#include<stdio.h> #include<stdlib.h> #include<string.h>void mallocSpace(char* p){p = (char*)malloc(100);memset(p,0,100);memcpy(p,"hello world",100); } int main(void){char* p = NULL;mallocSpace(p);printf("s = %s\n",p); }運行結果為:s=(null)
看起來代碼沒有什么問題,main中定義一個指針,將指針傳遞到函數中,給指針賦值,在main中再打印指針指向的字符串,也不存在方法出?;厥諆却娴膯栴},但是為什么會打印出來這樣的結果呢?
我們來捋一捋程序在內存中的過程:
首先第一行,定義了一個char類型的指針,指向了NULL
第二行,將指針p傳給了函數,進入函數,注意:此時的這個函數中的p和main中的p已經不是同一個p了(不信你打印一下兩個p的地址一樣不一樣)
進入函數,內存會在棧中加載函數體,加載參數p(新加載出來的),而且在函數中操作的p全部都是這個,跟main中的p一點關系都沒有了,所以最后打印出來會是null
為了證明,我自己加了兩個打印:
既然明白了問題出在哪里,解決辦法就是,要讓函數參數穿進去的p,和main中的p一樣了,那就傳p的地址吧,p已經是一個指針了,相應的函數參數應該是一個二級指針。
const全局變量和局部變量的區別
const全局變量在常量區,不能修改,用指針也不能修改。
const局部變量存放在棧上,不能直接修改,可以使用指針修改。
宏函數
#include<stdio.h> #include<stdlib.h> #include<string.h>#define MYADD(x,y) ((x)+(y))int add(int x,int y){return x+y; } int main(void){int a = 10;int b = 20;printf("a + b = %d\n",MYADD(a,b)); }以上代碼顯示的,add是一個正常的函數,MYADD就是一個宏函數了
說他是宏函數,其實他不是一個真正的函數
add函數才是真正的函數,因為他有返回值類型,參數,函數體
宏函數在一定場景下效率要比函數高
下面我們來分析一下,是在什么場景下,效率怎么個高的:
首先說普通函數調用流程:
假如,我在main中調用add函數計算結果,從main的第一行開始,過程為:
而宏函數沒有這么復雜,他只是在預處理的時候,進行簡單的替換文本,啥意思呢?我們之前學過編譯四部曲:預處理,編譯,匯編,鏈接。預處理的過程為:不檢查語法,宏定義展開,頭文件展開,就是這一步里面,他會把代碼中的
printf("a + b = %d\n",MYADD(a,b));直接替換為:
printf("a + b = %d\n",((a)+(b)) );就是這么簡單。省去了調用函數的時候,在棧里面的一切開銷,整個過程相當于把宏定義的內容搬過來了,增加了代碼量,是典型的空間換時間
所以說,對于頻繁使用,并且短小的函數,我們一般使用宏函數代替,因為宏函數沒有普通函數的開銷(壓棧,跳轉,返回等)
函數的調用慣例
以上,我們了解了一個函數的調用流程,但是存在兩個問題:
一個問題是上面的例子中函數的兩個參數,是哪個先入棧?
二一個問題是,最后函數結束,參數出棧,是誰管他們出棧的,我們說函數的調用者也可以,函數本身自己也可以,那么到底是由什么決定的呢?
其實,函數的調用者和被調用者對函數的調用有著一致的理解,例如,他們雙方都一致的認為函數的參數是按照某個固定的方式壓入棧中,如果不這樣的話函數將無法正確運行。
如果函數的調用方,在傳遞參數時,先壓入a參數,再壓入b參數,而被調用的函數則認為先壓入的b,再壓入a,那么被調用函數在使用ab的值時,就會顛倒。
因此函數的調用方和被調用方對于函數是如何調用的必須有一個明確的約定,只有雙方都遵循一樣的約定,函數才能被正確的調用。這樣的約定被稱為“調用慣例”
,一個調用慣例一般包擴以下幾個方面:
函數參數的傳遞順序和方式
函數的傳遞有很多種方式,最常見的是通過棧傳遞,函數的調用方將參數壓入棧中,函數自己再從棧中將參數取出,對于有多個參數的函數,調用慣例要規定函數調用方將參數壓棧的順序,從左到右,還是從右到左,有些調用慣例還允許使用寄存器傳遞參數,以提高性能
棧的維護方式
在函數將參數壓入棧中之后,函數體會被調用,此后需要將被壓入棧中的參數全部彈出,以使得棧在函數調用前后保持一致,這個彈出的工作可以由函數的調用方來完成,也可以由函數本身來完成
為了在鏈接的時候對調用慣例進行區分,調用慣例要對函數本身的名字進行修飾,不同的調用慣例有不同的名字修飾策略。
事實上,在C語言中,存在著多個調用慣例,而默認的是cdecl,任何一個沒有顯示指定調用慣例的函數都默認是cdecl慣例,比如我們上面對于add函數的聲明,他的完整寫法應該是:
注意:_cdecl不是標準的關鍵字,在不同的編譯器里可能有不同的寫法,例如gcc里就不存在_cdecl這樣的關鍵字,而是使用_attribute_((cdecl))
| cdecl | 函數調用方 | 從右至左參數入棧 | 下劃線+函數名 |
| stdcall | 函數本身 | 從右至左參數入棧 | 下劃線+函數名+@+參數字節數 |
| fastcall | 函數本身 | 前兩個參數由寄存器傳遞,其余參數通過堆棧傳遞 | @+函數名+@+參數的字節數 |
| pascal | 函數本身 | 從左至右參數入棧 | 較為復雜,詳見相關文檔 |
C++里面的調用慣例是thiscall
變量傳遞分析
上圖表示在main函數里面調用了A函數,而A函數又調用了B函數,那么,有幾個情況:
main函數在棧區定義了一個變量,函數A和函數B都可以使用
函數A在棧區定義了一個變量,main不可以使用,函數B可以使用
函數B在棧區定義了一個變量,main和函數A都不可以使用
歸納為:在棧中定義的變量,只要還沒有銷毀,就可以被別的函數使用
如果任意一個函數在堆區定義了一個變量,只要沒有free掉,任何函數都可以使用。
內存的存放方向
棧是開口向下的,越往里面放東西,地址是越來越低的
知道了棧的存放方向,那么內存的存放方向是什么樣的呢?換句話說,有一個int類型的數0xaabbccdd,在內存中占4個字節,那這4個字節在內存中是按照aa bb cc dd這樣排的呢?還是按照dd cc bb aa這么排的呢?
指針加1之后,說明內存是往高走的,打印結果也是慢慢往高位打印
這種存放方式叫做小端模式:高位字節存放在高地址,低位字節存放在低地址,
不是所有的系統存放數據都是小端模式,這里做一個了解。
上面的代碼在寫的時候,遇到一個問題,定義pchar的時候,如果使用 char* 打印結果就是:
使用unsigned char*定義pchar的時候,結果為:
原因單獨說,printf使用%x占位符打印signed char結果占4個字節?打印unsigned char結果占1個字節?
野指針
野指針指向一個已經刪除的對象,或未申請訪問受限內存區域的指針,與空指針不同,野指針無法通過簡單的判斷是否為NULL避免,只能通過良好的編程習慣來盡力減少,
什么情況下會導致野指針?
- 指針變量未初始化
任何指針變量剛被創建時不會自動成為NULL指針,他的缺省值是隨機的,會亂指一氣,所以指針變量在創建的時候應該初始化, - 指針釋放后未置空
有時指針在free或delete后,未置為NULL,便會使人以為是合法的,其實free尤其是delete,他們只是把指針所指的內存釋放掉,但并沒有把指針本身干掉,此時指針指向的就是垃圾內存,釋放后的指針應當立即置位NULL,防止野指針的出現 - 指針操作超越變量作用域
不要返回指向棧內存的指針或引用,因為棧內存在函數結束時會被釋放
指針的步長
看一個例子:
#include<stdio.h> #include<stdlib.h> #include<string.h>int main(void){int a = 100;char buf[64] = {0};memcpy(buf+1,&a,sizeof(int)); //不存放在第一個字節,從第二個字節開始存//取的時候怎么取呢?char* p = &buf;printf("%d\n",*(int*)(p+1));//指針+1,強轉成int類型的指針,再取值 }所以存的時候,怎么存的,要記住,取得時候要知道從哪里開始取
那么,如果情況復雜一點呢?
如果有一個結構體:
該怎么快速的知道我想要的字段偏移量是多少呢?
有一個函數,需要導一個頭文件
函數參數傳一個結構體,和一個字段名稱,就可以返回字段名稱的偏移量,
運行結果:
指針的間接賦值
通過地址強轉為指針再賦值
#include<stdio.h> #include<stdlib.h> #include<string.h>int main(void){int a = 10;printf("%p\n",&a);//28FF3C打印一個int類型的a的地址*(int*)0x28FF3C = 100; int類型的數,地址也是4個字節int類型,前面加上int*他就變成了一個指針,再前面加*取值,再賦值,就可以改變a的值printf("%d\n",a); }通過修改指針的值
#include<stdio.h> #include<stdlib.h> #include<string.h> void changePoint(int** val){ 函數使用二級指針接著*val = 0x8; 一級指針改值 } int main(void){int* p = NULL;printf("%x\n",p); 打印p的地址為0changePoint(&p); 將指針再次取地址,升級為二級指針,傳進函數printf("%x\n",p); 打印地址 結果為8 }指針做函數參數的輸入特性
主調函數分配內存,被調函數使用內存
在堆上分配內存
在棧上分配內存
#include<stdio.h> #include<stdlib.h> #include<string.h> void printArray(int* arr,int len){ 數組傳入參數,會退化為指針,指向第一個元素的位置for(int i = 0;i<len;i++){printf("%d\n",arr[i]);} } int main(void){int arr[] = {1,2,3,4,5}; 棧中定義一個數組int len = sizeof(arr)/sizeof(arr[0]); 計算出數組長度printArray(arr,len); 將數組和長度傳入函數中 }下面寫一個棧內存存放字符串的
#include<stdio.h> #include<stdlib.h> #include<string.h> void printStrings(char** strs,int len){ 我們說,普通類型的數組名當做函數參數,會退化為指針上一個例子中,int類型的數組,在函數參數中,要用int*來接著所以在這里,char*類型的數組,在函數參數中要用char**來接著,也就是多一層指針,for(int i = 0;i<len;i++){printf("%s\n",strs[i]);} } int main(void){char* strs[] = { 定義一個char*類型的數組,里面的每一個元素都是char*類型"aaaaa",//這些字母都是存儲在常量區"bbbbb","ccccc","ddddd","eeeee"};int len = sizeof(strs)/sizeof(strs[0]); 計算長度printStrings(strs,len); 傳參 }指針做函數參數的輸出特性
被調函數分配內存,主調函數使用內存
代碼示例:
總結
以上是生活随笔為你收集整理的C语言学习第017课——C语言提高(一)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 软件测试工程师 暴雪,前暴雪测试员爆《暗
- 下一篇: Autojs豆瓣小组自动回帖机器人,模拟