老師不講的C語言知識

老師不講的C語言知識

導語:

對於工科生,C語言是一門必修課。標準C(ANSI C)這個看似簡單的語言在硬件底層編程、嵌入式開發領域還是穩坐頭把交椅。在20年5月份,C語言就憑藉其在醫療設備上的廣泛應用,時隔五年重回編程語言榜首。

同學們在拿到學分之後還有沒有使用這門「手藝」呢?

想做軟硬件項目的同學還需要補足哪些知識呢?

不論是正在學習還是曾經學習過C語言的同學,這篇文章總結的一些要點能提供一個新的角度來理解C語言的設計理念和特性

一起來看看吧!

關鍵字

一共有多少個關鍵字?這個的確不好說,在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語言的實際應用。

希望大家都能夠熟練掌握這門傳統藝能

如果有關於編程語言、嵌入式等領域的想法想與我討論,歡迎各位來找我!
想看文章後續以及更多有關嵌入式、編程語言的分享歡迎關注公眾號。

公眾號.jpg

由於C標準的未定義情況較多,交由編譯器自己決定的情況非常多,不能保證在任何平台下都是同一結果。

作者能力有限,如有錯誤或者偏差,懇請各位指正!

歡迎轉載,請註明原文地址

Tags: