老師不講的C語言知識
導語:
對於工科生,C語言是一門必修課。**標准C(ANSI C)這個看似簡單**的語言在硬件底層編程、嵌入式開發領域還是穩坐頭把交椅。在20年5月份,C語言就憑借其在醫療設備上的廣泛應用,時隔五年重回編程語言榜首。
同學們在拿到學分之后還有沒有使用這門“手藝”呢?
想做軟硬件項目的同學還需要補足哪些知識呢?
不論是正在學習還是曾經學習過C語言的同學,這篇文章總結的一些要點能提供一個新的角度來理解C語言的設計理念和特性。
一起來看看吧!
[toc]
關鍵字
一共有多少個關鍵字?這個的確不好說,在C99和C11里都添加了新的關鍵字,也有的關鍵字由於過時淡出了我們的視線。下面這些關鍵字的用法都掌握了嗎?
auto
它可謂默默無聞,不少人應該知道它沒什么用——局部變量默認就是auto類型。除此之外,auto變量存放在動態儲存區中的棧區。也就是這種變量時有時無,壽命可變,自動(auto)管理。
注意:正因為來去自如,創建的局部變量不會自動初始化為零!!切記
但是,在C++11里auto翻身了,可以用作類型推導。比如:
//以前
double a=10.5;
//現在
auto a=10.5;
//這樣就可以把a作為double類型。
可是這和C有什么關系?
static
你能想到static的幾個用法?有人說是三個,我覺得就兩個。
- 修飾全局變量或函數時,它的作用是僅限本文件訪問。C語言里沒有命名空間,如果不加static,不同文件里的同名變量會引起混淆,畢竟他們的作用域相同。
- 修飾局部變量,成為存放在靜態儲存區的靜態變量。生命周期為整個程序執行過程。與auto不同,靜態變量在程序開始之前就初始化完畢。這也就是常說的第三個用法,將局部變量初始化為零。其實這只是變量存放在靜態儲存區的一個特征,所以不單獨拿出來。
volatile
大多數學校課程是不會用到它的,接觸單片機和多線程就能懂得它的重要性。
volatile的意思是”易變的、無常的“,名副其實。
volatile是對變量的修飾,比如volatile int flag=0;
它是對編譯器的提醒:
”嘿,這變量是變化無常的,你可小心點!“
這針對的是編譯器的”小聰明“——優化。
舉個例子
int flag=0;
...
flag=1;
if(flag==1)
計算機運算要先把變量從內存加載到寄存器,這一步是耗時間的。編譯器一看,前腳我才讓flag=1,這個flag還在寄存器里,到下一句判斷之間也沒有能改變flag的語句,那我不直接用這個寄存器里的flag=1嘛。
可萬萬沒想到,就在flag=1之后,if之前,來了一次硬件中斷,終端回調函數把flag改成0了。
編譯器是料不到的,就認為flag=1。這在很多實際情況下是很恐怖的。
不僅僅是中斷,插入一段匯編,其他線程改變內存都會引起這類問題。
如果改為volatile int flag=0;
,凡是用到這個變量,就會去內存里不怕麻煩地找到它,不再偷懶。
當時我年輕不懂事,一個單片機項目里用中斷改變標志位。怎么都不正常,后來哥們讓我在標志前面加個volatile就解決了。。。
__WEAK
這不是個關鍵字,這只是GCC的一個特性。看STM32官方固件庫的同學應該沒少見到它,但它不是C++里virtual那種虛函數。如果有同名的不帶_WEAK前綴的函數,優先使用不帶的。
如果用戶自定義了,那就使用用戶的,如果沒有,那就用默認的。這樣方便用戶自定義一些回調函數、處理函數。
類型
相信大家對C語言的強類型特性印象非常深刻。尤其是printf的格式化輸出和復雜指針的類型。
程序不就是數據結構+算法,基本類型則是構成數據結構高樓大廈的一磚一瓦。
char
冥冥之中,我覺得char類型是最神奇的類型。在C語言標准里char的大小是1 Byte,這是不會變的,也就是sizeof(char)無論在哪都是1。但是:
printf("%d\n",sizeof('a'));
輸出是多少?是4,一個int的大小!沒錯,字符常量的類型不是char而是int。
來放松一下。
你平時怎么讀“char”?反正我是讀了好幾年的**”差“**,后來轉念一想,字符的英文是character[ˈkærəktə(r)],那不應該是。。。。其實有三個發音,英文char(煤炭)、car(汽車)、care(關心)都可以。
float
浮點數比整形更貼近實際,也不至於出現除法去尾的情況。要注意的是,計算機的浮點數是分立的,有時候1.30會變成1.299999。比如matlab里查看eps(epsilon)可以得到浮點數的最小分度值。(win64下)
>>eps()
ans=2.2204e-16
結構體、數組、指針
三者的關系可以說是糾纏不清。
剛學C都遇到過,函數返回值可以是一個龐大的結構體,卻不能是一個簡單的數組。可是,數組類型可以是結構體,結構體的成員也可以包含數組,僅僅是組織方式的區別。
結構體和數組
舉例說明一下,現有結構體struct_a,有成員a、b、c三個。
struct {
int a;
int b;
int c;
}struct_a;
//訪問a
struct_a.a;
(&struct_a)->a;//千萬別寫&struct_a->a,->比&優先級高
(*(int*)(&struct_a)+0)//錯誤!!
//這是錯誤的,因為結構體的變量之間不保證連續,可能會有填充。
注意最后一種寫法,有時候編譯器為了對齊,會填充一些地址,導致不連續。不要這樣訪問結構體成員!
如果是數組呢?我們常用arr[n]這種方式來訪問數組成員,”[]“這個符號的用途是把a[b]變成*(a+b)。請結合例子理解一下。
int a[3]={1,2,3};
printf("%d \n",a[0]);
printf("%d \n",*(a+0));
printf("%d \n",0[a]);//輸出1
printf("%c",2["abc"])//輸出c
數組和指針
把數組傳入函數時,有兩種寫法
int func(int arr[]);
int func(int* arr);
在C里,第一種會自動轉化成第二種,所以訪問數組本質還是指針。
教你個竅門:
int arr[10];
int* ptr=&arr[-1];
然后就可以從下標1開始用數組ptr[]。
指針
毫無疑問,指針是個麻煩事。比如char *(*(*(a[2])())()
是一個包含2個指向返回 指向字符的指針的函數指針的數組,幾乎很難看出它到底是數組還是指針。
希望這些要點能幫到你!
優先與[]結合再與*結合
指針類型:把聲明中指針名稱去掉,就得到了指針的類型。
Int * ptr→int *
Int(* ptr)[3]→int(*)[3]
同時注意結合關系,比如下面這個的名稱就是ptr[3],而不是上面的ptr
int *ptr[3]->int *
所指向的類型:去掉指針名稱和一個*
int*ptr; : 指針所指向的類型是 int
int ** ptr; : 指針所指向的的類型是 int*
int(*ptr)[3]; : 指針所指向的的類型是 int()[3]
指針賦值時,左邊指針所指向的類型必須具有右邊指針所指向類型的全部限定符,比如
char* cp; const char* ccp; ccp=cp;//正確 cp=ccp;//錯誤
函數
C語言絕不是Python那樣自備電池的全能型語言,它是一門中級語言。
標准庫函數往往看起來簡陋而且有缺陷。
來了解一下吧。
函數調用順序
int a=f()+g()*h();
這三個函數的執行順序是不確定的,C標准把選擇順序的權力交給編譯器以便針對各個平台進行優化。
可以確定的一點是,乘法優先級高於加法。
參數壓棧順序:從右至左嗎?
可能很多人都知道這個考點,函數參數壓棧的順序是從右至左,右邊的表達式會先被運行。
重點在后面的問號
C標准對於壓棧順序並沒有明確規定,也就是編譯器可以修改成從左至右的壓棧順序。
默認的從右至左是為了支持可變參數,用來計算棧的大小。
確保你明白它們的用法、原理和......缺陷
早期的gets()
導致了蠕蟲病毒,因為它不檢查緩沖區是否越界,其實scanf也有這個問題。標准輸入輸出有許多設計上的細節需要被了解,比如printf使用%來轉義%而不是\ , scanf里的\n
並不代表等待一個換行符而是讀取並拋棄所有空格......
這些設計上的特點可能會在意想不到的地方產生出意想不到的效果。所以,為了程序的健壯性,多多了解它們吧。
預處理
預處理是個很好的想法,增強了程序的可移植性和裁剪性。
不僅僅是常見的#include、#define,它的功能可以非常強大,如果用得好的話。
#include能做些什么?
復制,原封不動的復制。
甚至可以這樣:
//str.h
"hu","xiao","an"
//main.c
char arr[3][10]={
#include"str.h"
};
printf(a[1]);//輸出xiao
真就是原封不動,但要注意預處理命令是要在一行的開頭,獨占一行。
如果是#include<>
則會首先在標准位置(C語言安裝位置)搜尋,#include ""
則現在同一文件夾下搜索,找不到再去標准位置搜索。
#define不好嗎?
在C++里,用#define來定義常量是不被推薦的,因為#define也僅僅是預處理替換,沒有類型檢查。
推薦使用const修飾的變量。仁者見仁,兩種方法各有特點。
結語
“看似簡單”這個描述對於C語言是再恰當不過的。最早的K&R標准只有40頁,ANSI C手冊則超過了兩百頁,盡管這樣,C語言特性給了編程者極大的自由,衍生出來許多意想不到的用法和Bug。。。
本文整理的內容不過是冰山一角,還有更多的進階內容等待探索。
周雖舊邦,其命唯新。多次的標准更新已經讓C語言不再是教科書里的簡陋模樣。了解新特征,有利於C語言的實際應用。
希望大家都能夠熟練掌握這門傳統藝能!
如果有關於編程語言、嵌入式等領域的想法想與我討論,歡迎各位來找我!
想看文章后續以及更多有關嵌入式、編程語言的分享歡迎關注公眾號。
由於C標准的未定義情況較多,交由編譯器自己決定的情況非常多,不能保證在任何平台下都是同一結果。
作者能力有限,如有錯誤或者偏差,懇請各位指正!
歡迎轉載,請注明原文地址