文章目錄
什麼是C
C 的發展史
C 關鍵字
命名空間
命名空間的定義
1.命名空間的普通定義
2.命名空間可以嵌套
3. 同一個工程中允許存在多個相同名稱的命名空間,編譯器最後會合成同一個命名空間中。
命名空間使用
1.加命名空間名稱及作用域限定符
2.使用using namespace 命名空間名稱引入
3.使用using将命名空間中的成員引入
C 中的輸入和輸出
缺省參數
全缺省
半缺省參數
函數重載
函數重載的原理
extern “C”
引用
引用的特征
1.引用在定義時必須初始化
2.一個變量可以有多個引用
3.引用一旦引用了一個實體,就不能再引用其他實體
常引用
引用的使用場景
1.引用做參數
2.引用做返回值
引用和指針的區别
内聯函數
特性
c 有哪些技術可以代替宏
auto關鍵字(C 11)
auto的使用細則
1.auto與指針和引用結合起來使用
2.在同一行定義多個變量
auto不能推導的場景
1.auto做為函數的參數
2.auto不能直接用來聲明數組
基于範圍的for循環(C 11)
範圍for的語法
範圍for的使用條件
1.for循環叠代的範圍必須是确定的
2. 叠代的對象要實現 和==的操作。
指針空值nullptr
C 98中的指針空值
C 11中的指針空值
什麼是C
C語言是結構化和模塊化的語言,适合處理較小規模的程序。對于複雜的問題,規模較大的程序,需要高度
的抽象和建模時,C語言則不合适。為了解決軟件危機,
20世紀80年代,
計算機界提出了OOP(object
oriented
programming:面向對象)思想,支持面向對象的程序設計語言應運而生。
1982年,Bjarne
Stroustrup博士在C語言的基礎上引入并擴充了面向對象的概念,發明了一種新的程序語
言。為了表達該語言與C語言的淵源關系,命名為C 。因此:C 是基于C語言而産生的,它既可以進行C語
言的過程化程序設計,又可以進行以抽象數據類型為特點的基于對象的程序設計,還可以進行面向對象的程
序設計。
C 的發展史
1979年,貝爾實驗室的本賈尼等人試圖分析unix内核的時候,試圖将内核模塊化于是在C語言的基礎上進行擴展,增加了類的機制,完成了一個可以運行的預處理程序,稱之為C
with classes。
語言的發展也是随着時代的進步,在逐步遞進的,讓我們來看看C 的曆史版本:
C 關鍵字
C 中總計有63個關鍵字:
其中畫圈的是C語言的關鍵字。這裡要注意了:false和true并不是C語言的關鍵字。
命名空間
在C/C 中,變量、函數和類都是大量存在的,這些變量、函數和類的名稱都将作用于全局作用域中,可能會導緻很多命名沖突。
使用命名空間的目的就是對标識符和名稱進行本地化,以避免命名沖突或名字污染,namespace關鍵字的出現就是針對這種問題的。
命名空間的定義
定義命名空間,需要使用到namespace關鍵字,後面跟命名空間的名字,然後接一對{}即可,{}中即為命名
空間的成員。
注意:一個命名空間就定義了一個新的作用域,命名空間中的所有内容都局限于該命名空間中
1.命名空間的普通定義
//1.
普通的命名空間,裡面可以定義變量,也可以定義函數
namespace
xjt
{
int printf = 1;
int rand = 2;
int Add(int a, int b)
{
return a b;
}
}
2.命名空間可以嵌套
//2.命名空間可以嵌套
namespace
xjt
{
int printf = 1;
int rand = 2;
int Add(int a, int b)
{
return a b;
}
namespace xjt2
{
int a = 0;
int Sub(int a, int b)
{
return a - b;
}
}
}
3. 同一個工程中允許存在多個相同名稱的命名空間,編譯器最後會合成同一個命名空間中。
//3.
同一個工程中允許存在多個相同名稱的命名空間,編譯器最後會合成同一個命名空間中。
namespace
xjt
{
int a = 3;
int b = 1;
}
它會與上面的xjt命名空間合并
命名空間使用
下面來看這麼一段代碼
namespace
xjt
{
int printf = 1;
int rand = 2;
int Add(int a, int b)
{
return a b;
}
}
#include
很顯然直接打印printf是不可能的,因為你這樣調用的是printf的地址,所以會出現的這樣的結果,正面的調用方法為以下三種。
1.加命名空間名稱及作用域限定符
符号“::”在C 中叫做作用域限定符,我們通過“命名空間名稱::命名空間成員”便可以訪問到命名空間中相應的成員
2.使用using namespace 命名空間名稱引入
但是這種方式存在着一些弊端,如果我們在命名空間中定義了一個名字為printf的變量,那麼之後再将namespace
xjt這個命名空間引入的話,就會造成命名的污染了。
3.使用using将命名空間中的成員引入
這種方法可以防止命名的污染,因為它隻引入了一部分。
C 中的輸入和輸出
新生嬰兒會以自己獨特的方式向這個嶄新的世界打招呼,C 剛出來後,也算是一個新事物,那C 是否也應該向這個美好的世界來聲問候呢?我們來看下C 是如何來實現問候的。
#include
在C語言中有标準輸入輸出函數scanf和printf,而在C 中有cin标準輸入和cout标準輸出。在C語言中使用scanf和printf函數,需要包含頭文件stdio.h。在C 中使用cin和cout,需要包含頭文件iostream以及std标準命名空間。
C 的輸入輸出方式與C語言更加方便,因為C 的輸入輸出不需要控制格式,例如:整型為%d,字符型為%c。
#include
注意:endl,這其中的l不是阿拉伯數字1,而是26個英文字母的l,它的作用相當于換行。
這裡我們還要注意下cin的特點,他和C語言中的gets有些像,gets是遇到換行符停止,而cin是以遇到空格,tab或者換行符作為分隔符的,因此這兒輸入hello
world會被空格符分隔開來。
缺省參數
缺省參數是聲明或定義函數時為函數的參數指定一個默認值。在調用該函數時,如果沒有指定實參則采用該
默認值,否則使用指定的實參。
//缺省參數
#include
全缺省
全缺省參數,即函數的全部形參都設置為缺省參數。
//全缺省
#include
半缺省參數
void
func(int a, int b, int c = 2)
{
cout << a << endl;
cout << b << endl;
cout << c << endl;
}
注意:
1、半缺省參數必須從右往左依次給出,不能間隔着給。
//錯誤示例
void
func(int a, int b = 2, int c)
{
cout << a << endl;
cout << b << endl;
cout << c << endl;
}
2、缺省參數不能在函數聲明和定義中同時出現
//錯誤示例
//test.h
void
func(int a, int b, int c = 3);
//test.c
void
func(int a, int b, int c = 2)
{
cout << a << endl;
cout << b << endl;
cout << c << endl;
}
因為:如果聲明與定義位置同時出現,恰巧兩個位置提供的值不同,那編譯器就無法确定到底該用那
個缺省值。
3、缺省值必須是常量或者全局變量。
//正确示例
int
x = 3;//全局變量
void
func(int a, int b = 2, int c = x)
{
cout << a << endl;
cout << b << endl;
cout << c << endl;
}
函數重載
函數重載:是函數的一種特殊情況,C 允許在同一作用域中聲明幾個功能類似的同名函數,這些同名函數的
形參列表(參數個數
或 類型 或 順序)必須不同,常用來處理實現功能類似數據類型不同的問題
#include
注意:若僅僅隻有返回值不同,其他都相同,則不構成函數重載。
short
Add(short left, short right)
{
return left right;
}
int
Add(short left, short right)
{
return left right;
}
函數重載的原理
為什麼C 支援函數重載,而C語言不可以了?
這裡我們就要回顧一下以前的知識了,在運行到執行文件前,要經過:預編譯,編譯,彙編,鍊接這些階段
其實問題就出在編譯完之後的彙編階段,因為在這裡C 和C語言有着些許的不同,下面我們來看看:
采用C語言編譯器編譯之後
采用C 編譯器編譯之後
總結:
1.其實歸根到底,還是因為C編譯器和C 編譯器對函數名的修飾不同。在gcc下的修飾規則是:【_Z 函數長度 函數名 類
型首字母】。
2.這其實也告訴我們為什麼函數的返回類型不同,不會構成函數重載,因為修飾規則并不會受返回值的影響。
extern “C”
有時候在C 工程中可能需要将某些函數按照C的風格來編譯,在函數前加extern
“C”,意思是告訴編譯器,
将該函數按照C語言規則來編譯。比如:tcmalloc是google用C 實現的一個項目,他提供tcmallc()和tcfree
兩個接口來使用,但如果是C項目就沒辦法使用,那麼他就使用extern
“C”來解決。
引用
引用不是新定義一個變量,而是給已存在變量取了一個别名,編譯器不會為引用變量開辟内存空間,它和它
引用的變量共用同一塊内存空間。
類型& 引用變量名(對象名) = 引用實體;
#include
注意:引用類型必須和引用實體是同種類型的
引用的特征
1.引用在定義時必須初始化
//正确示例
int
a = 10;
int&
b = a;//引用在定義時必須初始化
//錯誤示例
int
a = 10;
int
&b;//定義時未初始化
b
= a;
2.一個變量可以有多個引用
int
a = 10;
int&
b = a;
int&
c = a;
int&
d = a;
3.引用一旦引用了一個實體,就不能再引用其他實體
int
a = 10;
int& b = a;
int c = 20;
b = c;//你的想法:讓b轉而引用c
但實際的效果,确實将c的值賦值給b,又因為b是a的引用,所以a的值見解變成了20。
常引用
上面提到,引用類型必須和引用實體是同種類型的。但是僅僅是同種類型,還不能保證能夠引用成功,這兒我們還要注意可否可以修改的問題。
void
TestConstRef()
{
const int a = 10;
//int& ra = a; // 該語句編譯時會出錯,a為常量
const int& ra = a;
// int& b = 10; // 該語句編譯時會出錯,b為常量
const int& b = 10;
double d = 12.34;
//int& rd = d; // 該語句編譯時會出錯,類型不同
const int& rd = d;
}
這裡的a,b,d都是常量,常量是不可以被修改的,但是如果你用int&ra等這樣來引用a的話,那麼引用的這個a是可以被修改的,因此會出問題。
下面我們來看這麼一段代碼:
#include
這個引用對嗎?想要弄明白這個問題,首先要明白隐士類型提升的問題,在這裡int到double存在隐士類型的提升,而在提升的過程中系統會創建一個常量區來存放a類型提升後的結果。因此到這兒,這段代碼一看就是錯了,因為你隐士類型提升時a是存放在常量區中的,常量區是不可以被修改的,而你用double&ra去引用他,ra這個引用是可以被修改的。
加個const就可以解決這個問題。
#include
注意:将不可修改的量用可讀可寫的量來引用是不可以的,但是反過來是可以的,将可讀可寫的量用隻可讀的量來引用是可以的。
引用的使用場景
1.引用做參數
還記得C語言中的交換函數,學習C語言的時候經常用交換函數來說明傳值和傳址的區别。現在我們學習了引用,可以不用指針作為形參了。因為在這裡a和b是傳入實參的引用,我們将a和b的值交換,就相當于将傳入的兩個實參交換了。
//交換函數
void
Swap(int& a, int& b)
{
int tmp = a;
a = b;
b = tmp;
}
2.引用做返回值
當然引用也能做返回值,但是要特别注意,我們返回的數據不能是函數内部創建的普通局部變量,因為在函數内部定義的普通的局部變量會随着函數調用的結束而被銷毀。我們返回的數據必須是被static修飾或者是動态開辟的或者是全局變量等不會随着函數調用的結束而被銷毀的數據。
不加static的後果
你是不是疑惑為什麼打印的不是2而是7了?
這人就更奇怪了,為什麼中間加了一句printf,就打印随機值了?
下面我們來看看分析:
為什麼會出現随機值,因為你在函數裡定義的變量是臨時變量,出了函數函數是會銷毀的,這時它就随機指向内存中的一塊空間了。所以在引用做函數返回值時最好還是給在函數中定義的變量加上static。
這時你覺得你真的懂這段代碼了嗎?
#include
可能你會好奇了?為什麼這兒是3了?下面來看看分析
其實你換種寫法,這兒的結果就會換成7,原因也很簡單,正是上面圖片中說的原因
注意:如果函數返回時,出了函數作用域,返回對象還未還給系統,則可以使用引用返回;如果已經還給系統了,則必須使用傳值返回。
這句話說的是下面這種例子:
int&
Add(int a, int b)
{
int c=a b; //出了函數作用域,c不在,回給了系統
return c;
}
int&
Add(int a,int b)
{
static c=a b; //出了函數作用域,c還在,可以用引用返回
return c;
}
大家是不是感覺這個傳引用返回用起來很怪了,下面我們來分析一下它是如何返回的。
總結:
傳值的過程中會産生一個拷貝,而傳引用的過程中不會,其實在做函數參數時也具有這個特點。
引用和指針的區别
在語法概念上引用就是一個别名,沒有獨立空間,和其引用實體共用同一塊空間。
int
main()
{
int a = 10;
int& ra = a;
cout<<"&a = "<<&a<<endl;
cout<<"&ra = "<<&ra<<endl;
return 0; }
在底層實現上實際是有空間的,因為引用是按照指針方式來實現的。
int
main()
{
int a = 10;
int& ra = a;
ra = 20;
int* pa = &a;
*pa = 20;
return 0; }
我們來看下引用和指針的彙編代碼對比
引用和指針的區别
1、引用在定義時必須初始化,指針沒有要求。
2、引用在初始化時引用一個實體後,就不能再引用其他實體,而指針可以在任何時候指向任何一個同類型實體。
3、沒有NULL引用,但有NULL指針。
4、在sizeof中的含義不同:引用的結果為引用類型的大小,但指針始終是地址空間所占字節個數(32位平台下占4個字節)。
5、引用進行自增操作就相當于實體增加1,而指針進行自增操作是指針向後偏移一個類型的大小。
6、有多級指針,但是沒有多級引用。
7、訪問實體的方式不同,指針需要顯示解引用,而引用是編譯器自己處理。
8、引用比指針使用起來相對更安全。
内聯函數
概念:以inline修飾的函數叫做内聯函數,編譯時C 編譯器會在調用内聯函數的地方展開,沒有函數壓棧的開銷,
内聯函數提升程序運行的效率。(看到在加粗部分時,小夥伴肯定會想,這和c語言中的宏是不是很像了?)
如果在上述函數前增加inline關鍵字将其改成内聯函數,在編譯期間編譯器會用函數體替換函數的調用
特性
inline是一種以空間換時間的做法,省去調用函數額開銷。所以代碼很長/遞歸的函數不适宜
使用作為内聯函數。
inline對于編譯器而言隻是一個建議,編譯器會自動優化,如果定義為inline的函數體内代碼比較長/遞歸等
等,編譯器優化時會忽略掉内聯。
inline不建議聲明和定義分離,分離會導緻鍊接錯誤。因為inline被展開,就沒有函數地址了,鍊接就會
找不到。
//F.h
#include
c 有哪些技術可以代替宏
C 有哪些技術替代宏?
常量定義 換用const
函數定義 換用内聯函數
auto關鍵字(C 11)
在早期的C/C 中auto的含義是:使用auto修飾的變量是具有自動存儲器的局部變量,但遺憾的是一直沒有人去使用它。
在C 11中,标準委員會賦予了auto全新的含義:auto不再是一個存儲類型指示符,而是作為一個新的類型指示符來指示編譯器,auto聲明的變量必須由編譯器在編譯時期推導而得。
可能光看這一句話,你不一定能懂,下面我們舉幾個例子。
#include
注意:使用auto定義變量時必須對其進行初始化,在編譯階段編譯器需要根據初始化表達式來推導auto的實際類
型。因此auto并非是一種“類型”的聲明,而是一個類型聲明時的“占位符”,編譯器在編譯期會将auto替換為
變量實際的類型。
auto的使用細則
1.auto與指針和引用結合起來使用
用auto聲明指針類型時,用auto和auto*沒有任何區别,但用auto聲明引用類型時則必須加&
#include
注意:用auto聲明引用時必須加&,否則創建的隻是與實體類型相同的普通變量,隻不過将其換了個姓名而已。
2.在同一行定義多個變量
當在同一行聲明多個變量時,這些變量必須是相同的類型,否則編譯器将會報錯,因為編譯器實際隻對
第一個類型進行推導,然後用推導出來的類型定義其他變量。
void
TestAuto()
{
auto a = 1, b = 2;
auto c = 3, d = 4.0; // 該行代碼會編譯失敗,因為c和d的初始化表達式類型不同
}
auto不能推導的場景
1.auto做為函數的參數
//
此處代碼編譯失敗,auto不能作為形參類型,因為編譯器無法對a的實際類型進行推導
void
TestAuto(auto a)
{}
2.auto不能直接用來聲明數組
void
TestAuto()
{
int a[] = {1,2,3};
auto b[] = {4,5,6};
}
為了避免與C 98中的auto發生混淆,C 11隻保留了auto作為類型指示符的用法
auto在實際中最常見的優勢用法就是跟以後會講到的C 11提供的新式for循環,還有lambda表達式等
進行配合使用。
基于範圍的for循環(C 11)
範圍for的語法
在C 98中如果要遍曆一個數組,可以按照以下方式進行:
void
TestFor()
{
int array[] = { 1, 2, 3, 4, 5 };
//将數組所有元素乘以2
for (int i = 0; i < sizeof(array) / sizeof(array[0]); i)
array[i] *= 2;
for (int* p = array; p < array sizeof(array)/ sizeof(array[0]);
p)
cout << *p << endl; }
對于一個有範圍的集合而言,由程序員來說明循環的範圍是多餘的,有時候還會容易犯錯誤。因此C 11中
引入了基于範圍的for循環。for循環後的括号由冒号“
:”分為兩部分:第一部分是範圍内用于叠代的變量,
第二部分則表示被叠代的範圍。
注意不能寫成auto,不然改變不了原數組
正确的寫法
void
TestFor()
{
int array[] = { 1, 2, 3, 4, 5 };
//将數組中所有元素乘以2
for(auto& e : array)
e *= 2;
for(auto e : array)
cout << e << " ";
return 0;
}
注意:與普通循環類似,可用continue來結束本次循環,也可以用break來跳出整個循環。
範圍for的使用條件
1.for循環叠代的範圍必須是确定的
對于數組而言,就是數組中第一個元素和最後一個元素的範圍;對于類而言,應該提供begin和end的
方法,begin和end就是for循環叠代的範圍。
注意:以下代碼就有問題,因為for的範圍不确定
void
TestFor(int array[])
{
for(auto& e : array) //這裡的array其實不是數組,數組在傳參時會退化成指針
cout<< e <<endl; }
2. 叠代的對象要實現 和==的操作。
關于叠代器這個問題,以後會講,現在大家了解一下就可以了。
指針空值nullptr
C 98中的指針空值
在良好的C/C 編程習慣中,在聲明一個變量的同時最好給該變量一個合适的初始值,否則可能會出現不可預料的錯誤。比如未初始化的指針,如果一個指針沒有合法的指向,我們基本都是按如下方式對其進行初始化:
int*
p1 = NULL;
int*
p2 = 0;
NULL其實是一個宏,在傳統的C頭文件(stddef.h)中可以看到如下代碼:
#ifndef
NULL
#ifdef
__cplusplus
#define
NULL 0
#else
#define
NULL ((void *)0)
#endif
#endif
可以看到,NULL可能被定義為字面常量0,或者被定義為無類型指針(void*)的常量。不論采取何種定義,在
使用空值的指針時,都不可避免的會遇到一些麻煩,比如:
#include
程序本意本意是想通過Fun(NULL)調用指針版本的Fun(int* p)函數,但是由于NULL被定義為0,Fun(NULL)最終調用的是Fun(int p)函數。
注:在C 98中字面常量0,既可以是一個整型數字,也可以是無類型的指針(void*)常量,但編譯器默認情況下将其看成是一個整型常量,如果要将其按照指針方式來使用,必須對其進行強制轉換。
C 11中的指針空值
對于C 98中的問題,C 11引入了關鍵字nullptr。
在使用nullptr表示指針空值時,不需要包含頭文件,因為nullptr是C 11作為關鍵字引入的。
在C 11中,sizeof(nullptr)與sizeof((void*)0)所占的字節數相同,大小都為4。
,
更多精彩资讯请关注tft每日頭條,我们将持续为您更新最新资讯!