您好,登錄后才能下訂單哦!
C++的初步學習有以下幾個方面
我們知道,在c中有32個關鍵字,而c++中有63個關鍵字
分別為
為什么會有命名空間,他的作用是什么?
在一個大的工程里,要定義很多變量和函數,若將這些變量和函數都定義在全局作用域中,一不小心就可能出現重復定義的情況。因而引入命名空間的概念,其目的是對標識符名稱進行本地化,以避免命名沖突或名字沖突。
命名空間是什么?
一個命名空間就定義了一個新的作用域,命名空間中的所有內容都局限于該命名空間里。命名空間里可有變量、函數、結構體、另一個命名空間等等普通在全局定義的命名空間里都可以有。在不同的命名空間里可以使用一個變量名。以后在使用某個命名空間里的某個變量,引入就可以了。這樣定義變量時,就不用考慮之前這個名字有沒有用過,只用看在這個命名空間里存不存在該變量。
命名空間的定義
定義命名空間,需要使用到namespace關鍵字,后面跟命名空間的名字,然后接一對{}即可,{}中即為命名空間的成員。命名空間的定義有以下三種形式:
//1.普通定義
namespace N1 // N1為命名空間的名稱
{
int a;
int Add(int left, int right)
{
return left + right;
}
}
//2.嵌套定義
namespace N2
{
int a;
int b;
int Add(int left, int right)
{
return left + right;
}
namespace N3
{
int c;
int d;
int Sub(int left, int right)
{
return left - right;
}
}
}
//3.重復的定義
namespace N1{int a};
namespace N1{int b};
//在編譯時,編譯器會自動將其合并為一個命名空間,在定義的時候也可將其看做同一個命名空間,因而同名命名空間不要使用相同變量
命名空間的使用
在命名空間里定義的內容是不可以直接使用的。
引用一個操作符 ‘::’ 作用域限定符用于在作用域外引用作用域里的內容
引用一個關鍵字:在一個作用域中使用 using 將另一個命名空間里的想要的內容拿出來,方便下面使用
使用方式有以下三種:
//1.加命名空間名稱及作用域限定符
namespace N
{
int a;
int b;
}
int main{
printf("%d\n", N::a); 打印N中的a
return 0;
}
//2.使用using將命名空間中成員引入
using N::b;
int main()
{
printf("%d\n", N::a); //并沒引入a
printf("%d\n", b); //在此的b就可以直接使用了
return 0;
}
// 3.使用using namespace 命名空間名稱引入
using namespce N; //將N 中所有的內容都引入
int main()
{
printf("%d\n", a);
printf("%d\n", b);
return 0;
}
輸出函數:cout標準輸出(控制臺)類似于printf
輸入函數:cin標準輸入(鍵盤)類似于scanf
兩個函數屬于標準庫 iostream 再引入命名空間std
用法:他們的用法比printf和scanf要靈活,輸出不用再加%d..來說明輸出/輸入什么類型的值,可連接各種類型的值
例如如下代碼
#include <iostream>
using namespace std;
int main()
{
int a;
double b;
char c;
cin>>a;
cin>>b>>c;
cout<<a<<endl;
cout<<b<<" "<<c<<endl;
return 0;
}
概念:缺省參數是聲明或定義函數時為函數的參數指定一個默認值。在調用該函數時,如果沒有指定實參則采用該默認值,否則使用指定的實參。例如:
void TestFunc(int a = 0)
{
cout<<a<<endl;
}
int main()
{
TestFunc(); // 沒有傳參時,使用參數的默認值 0
TestFunc(10); // 傳參時,使用指定的實參
}
在一個函數的形參列表中,我們可以給一部分形參默認值,也可以全給。因此分為半缺省參數和全缺省參數,用法及要求如下
全缺省參數:每個形參都賦了缺省值
void TestFunc(int a = 10, int b = 20, int c = 30)
{
cout<<"a = "<<a<<endl;
cout<<"b = "<<b<<endl;
cout<<"c = "<<c<<endl;
}
int main()
{
TestFunc(); //10 20 30
TestFunc(1); //1 20 30
TestFunc(1,2); // 1 2 30
//為什么把1給a呢?我們從半缺省參數用法里找答案
}
半缺省參數:不是所有的形參都賦了缺省值,但賦半缺省參數有一定規則: 半缺省參數必須從右往左依次來給出,不能間隔著給,就是前面的可以省略,但一旦給值,后面的都必須都給值 。因此
void TestFunc(int a, int b = 10, int c = 20)√
void TestFunc(int a=10, int b , int c = 20) ×
void TestFunc(int a=10, int b=20 , int c ) ×
通過半缺省參數的規則,我們可回答為什么全缺省參數給值是從前往后給的:半缺省參數前面的可以省略,所以在不知道函數是不是半缺省參數的情況下,實參要賦從第一個形參開始賦值
定義:在同一作用域中聲明幾個功能類似的同名函數,這些同名函數的形參列表(參數個數 或 類型 或 順序)必須不同,常用來處理實現功能類似數據類型不同的問題。
如以下代碼:
int Add(int left, int right)
{
return left+right;
}
double Add(double left, double right)
{
return left+right;
}
long Add(long left, long right)
{
return left+right;
}
int main()
{
Add(10, 20);
Add(10.0, 20.0);
Add(10L, 20L); //通過實參類型來找函數
return 0;
}
注:函數不可僅靠返回值類型來實現重載
short Add(short left, short right)
{
return left+right;
}
int Add(short left, short right)
{
return left+right;
}
//這兩個函數無法實現重載
void TestFunc(int a = 10);
void TestFunc( );
//這兩個函數就無法形成重載,在另一個函數中調用TestFunc( ),編譯器不知道要調用哪一個;
缺省函數與普通函數無法形成重載,例如:
void TestFunc(int a = 10);
void TestFunc(int a );
//這兩個函數就無法形成重載,在另一個函數中調用TestFunc(num ),編譯器不知道要調用哪一個;
因而:想要形成函數重載,要確保兩個函數在調用的時候不會起沖突,不會出現在傳某個值的時候,兩個函數都可以調的情況。
我們知道:c語言中不可以實現函數重載,為什么c++中可以呢?因為在程序編譯時,編譯器會對每個函數名進行命名修飾,下面我們來引入命名修飾的概念
在c++程序編譯時,編譯器為區分各個函數,會將函數、變量名重新改變,使每個函數名成為全局唯一的名稱,將參數類型包含在最終的名字中,因而通過形參列表的不同可以將同名函數進行區分,就可保證名字在底層的全局唯一性。
那么c++中具體將名字修改成什么樣子了呢?
有如下代碼:
int Add(int left, int right);
double Add(double left, double right);
int main()
{
Add(1, 2);
Add(1.0, 2.0);
return 0;
}
//在vs下,對上述代碼進行編譯鏈接,最后編譯器報錯:
//error LNK2019: 無法解析的外部符號 "double cdecl Add(double,double)" (?Add@@YANNN@Z)
// error LNK2019: 無法解析的外部符號 "int __cdecl Add(int,int)" (?Add@@YAHHH@Z)
通過上述錯誤可以看出,編譯器實際在底層使用的不是Add名字,而是被重新修飾過的一個比較復雜的名字,被重新修飾后的名字中包含了:函數的名字以及參數類型。
visual stdio 下c++的修飾規則:
通過以上簽名及修飾后的名字可推得命名方式:
修飾后名字由“?”開頭,接著是函數名由“@"符號結尾的函數名:后面跟著由“@"結尾的類名“C”和名稱空間“N”,再一個“@”表示函數的名稱空間結束:第一個“A”表示函數調用類型為“_ cdecl” ,接著是函數的參數類型及返回值,由“@”結束,最后由“Z”結尾。其中A后面第一個是返回值類型,然后接下來到@之前都是形參的類型,H表示int,M表示float
那為什么c語言中,同名函數為什么不能構成重載呢?
因為c語言中的名字修飾只是在函數名前加了個下劃線,形參列表并未參與名字修飾,因而不能夠通過形參列表來區分各個同名函數。
在某個函數前加extern “C”,可將c++工程中某些函數按c的風格來編譯
概念:給變量取了個別名,和變量共用一塊內存空間,可以通過引用來改變變量。
定義:類型& 引用變量名=引用實體
注意:引用類型必須和引用實體的類型必須相同。
如:
int a = 10;
int& ra = a;//定義引用類型
printf("%p\n", &a);
printf("%p\n", &ra); //結果相同
引用特性:
1>引用在定義時必須初始化,不能存在空著的引用
int& ra ;//會發生錯誤
//起了外號,這個外號又不是任何人的,這個外號存在有什么意義?
2>一個變量可有多個引用(一個人可以起很多個別名)
3>引用一旦引用一個實體,再不能引用其他實體
int a=0;
int b=1;
int& ra=a;
ra=b; //ra不是改變了引用,只是將b的值賦給ra
printf("%d",a); //->1
常引用
const int a = 10;
int& ra = a; // 該語句編譯時會出錯,a為常量
//const修飾的變量,引用前也要加const,若不加,那么就可以通過引用修改變量的值了。
const int& ra = a;//正確寫法
int& b = 10; // 該語句編譯時會出錯,10為常量
//引用不能做常數的引用,要引用前面加const,常熟也是不能夠被修改的
const int& b = 10;
double d = 12.34;
int& rd = d; // 該語句編譯時會出錯,類型不同
const int& rd = d;//這個是正確的的,但rd并不是d的別名
//而是先通過a來形成一個臨時變量存放a的整數部分,然后ra引用這個臨時變量。但是該臨時變量不知道名字,也不知道地址,因而也修改不了,該臨時變量具有一定的常性,因而要在ra前加const
引用使用場景
1>做參數:函數形參設為引用類型
void Swap(int& left, int& right)
{
int temp = left;
left = right;
right = temp;
}
說明:如果想要通過形參改變實參,可將形參設為普通類型 如果不想要通過形參改變實參,可將形參設為const類型。
效率:傳值的效率低于傳址、傳引用效率。傳地址和傳引用時間相同。因為傳引用和傳指針的過程在內存中的變化其實是一樣的,傳引用的過程在編譯時,會轉成傳指針的形式,在編譯過程中,引用是按照指針方式來實現的
#include <time.h>
struct A
{
int a[10000];
};
void TestFunc1(A a)
{}
void TestFunc2(A& a)
{}
void TestRefAndValue()
{
A a;
// 以值作為函數參數
size_t begin1 = clock();
for (size_t i = 0; i < 10000; ++i)
TestFunc1(a);
size_t end1 = clock();
// 以引用作為函數參數
size_t begin2 = clock();
for (size_t i = 0; i < 10000; ++i)
TestFunc2(a);
size_t end2 = clock();
// 分別計算兩個函數運行結束后的時間
cout << "TestFunc1(int*)-time:" << end1 - begin1 << endl;
cout << "TestFunc2(int&)-time:" << end2 - begin2 << endl;
}
// 運行多次,檢測值和引用在傳參方面的效率區別
//結果都很小,而且相差無幾
//反匯編后,可看到傳引用的過程和傳指針的過程一模一樣。
int main()
{
for (int i = 0; i < 10; ++i)
{
TestRefAndValue();
}
return 0;
}
2>做返回值:將返回值類型設為引用類型
int& TestRefReturn(int& a)
{
a += 10;
return a;
}
注意:如果函數返回時,離開函數作用域后,其棧上空間已經還給系統,因此不能用棧上的空間作為引用類型返回。因此,引用作為返回值,返回變量不應受函數控制,即函數結束,變量的生命周期存在。比如:全局變量,static修飾的局部變量,用戶未釋放的堆,引用類型參數
發生該錯誤有以下代碼:
int& Add(int a, int b)
{
int c = a + b;
return c;
}
//在函數調用完后,棧上的c占用的那一塊空間就被釋放了(可以覆蓋),因此就沒什么意義了
int main()
{
int& ret = Add(1, 2);
Add(3, 4);
cout << "Add(1, 2) is :"<< ret <<endl;
//->7,Add(3, 4)將c的那一塊空間又覆蓋掉了
return 0;
}
通過比較,發現傳值和指針在作為傳參以及返回值類型上效率相差很大,因而可以讓引用作為返回值的地方就用引用,除非是要返回一個函數中定義的變量(該變量的空間會隨函數調用完而變得無效)要返回值外,其他情況都可用引用返回。
#include <time.h>
struct A
{
int a[10000];
};
A a;
A TestFunc1()
{
return a;
}
A& TestFunc2()
{
return a;
}
void TestReturnByRefOrValue()
{
// 以值作為函數的返回值類型
size_t begin1 = clock();
for (size_t i = 0; i < 100000; ++i)
TestFunc1();
size_t end1 = clock();
// 以引用作為函數的返回值類型
size_t begin2 = clock();
for (size_t i = 0; i < 100000; ++i)
TestFunc2();
size_t end2 = clock();
// 計算兩個函數運算完成之后的時間
cout << "TestFunc1 time:" << end1 - begin1 << endl;
cout << "TestFunc2 time:" << end2 - begin2 << endl;
}
// 測試運行10次,值和引用作為返回值效率方面的區別
int main()
{
for (int i = 0; i < 10; ++i)
TestReturnByRefOrValue();
return 0;
}
引用與指針
在語法概念上引用就是一個別名,沒有獨立空間,和其引用實體共用同一塊空間,但在底層實現上實際是有空間的,因為引用是按照指針方式來實現的。
int main()
{
int x = 10;
int& rx = x;
rx = 20;
int* px = &x;
*px = 20;
return 0;
}
對于該代碼我們來看反匯編代碼:
可發現,在內存中兩者在底層的使用方式是一樣的,引用也是按照指針方式來實現的
那兩者又有什么不同呢?
1> 引用在定義時必須初始化,指針沒有要求。因而指針需要判空,而引用不用,因為引用定義時就初始化了
2> 引用在初始化時引用一個實體后,就不能再引用其他實體,而指針可以在任何時候指向任何一個同類型實體
3> 沒有NULL引用,但有NULL指針
4>在sizeof中含義不同:引用結果為引用類型的大小,但指針始終是地址空間所占字節個數(32位平臺下占4個字節)
5>引用自加即引用的實體增加1,在連續的空間中指針自加即指針向后偏移一個類型的大小
6>有多級指針,但是沒有多級引用
7> 訪問實體方式不同,指針需要顯式解引用,引用編譯器自己處理
8> 引用比指針使用起來相對更安全。
概念:以inline修飾的函數叫做內聯函數,編譯時C++編譯器會在調用內聯函數的地方展開,沒有函數壓棧的開銷,內聯函數提升程序運行的效率。
普通函數會進行壓棧形成棧幀等操作
而內聯函數在編譯時會直接將調用函數換為函數內部的操作
查看方式:1. 在release模式下,查看編譯器生成的匯編代碼中是否存在call Add2. 在debug模式下,需要對編譯器進行設置,否則不會展開(因為debug模式下,編譯器默認不會對代碼進行優化,給出vs2013的設置方式):功能->屬性->配置->c/c++->將常規中的調試信息格式改為程序數據庫,再將優化中的內聯函數擴展改為只適用于_inline
特性
1> inline是一種以空間換時間的做法。所以代碼很長或者有循環/遞歸的函數不適宜使用作為內聯函數。
2>inline對于編譯器而言只是一個建議,編譯器會自動優化,如果定義為inline的函數體內有循環/遞歸等等,編譯器優化時會忽略掉內聯。
3>inline不建議聲明和定義分離,分離會導致鏈接錯誤。因為inline被展開,就沒有函數地址了,鏈接就會找不到。因而內聯函數具有文件作用域,只在本文件有用,其他文件不可用。
// F.h
#include <iostream>
using namespace std;
inline void f(int i);
// F.cpp
#include "F.h"
void f(int i)
{
cout << i << endl;
}
// main.cpp
#include "F.h"
int main()
{
f(10);
return 0;
}
// 鏈接錯誤:main.obj : error LNK2019: 無法解析的外部符號 "void __cdecl f(int)" (?
f@@YAXH@Z),該符號在函數 _main 中被引用
內聯函數與const、宏
在c++中,const修飾的變量有常量的特性也有宏的特性,在編譯時會發生替換和檢測,即使通過指針修改也無法改變變量值。有如下代碼
const int a=1;
int *pa=(int *)a;
*pa=2;
printf("%d,%d",*pa,a);
//結果為2,1 a仍然沒有修改
而在c中是可以的,因為c中是不會檢測的,通過指針也是修改const變量的
宏是在預處理時替換的,不參與編譯,也不可調試。
宏的優點:增強代碼的復用性。提高性能。
缺點:
1>不方便調試宏。(因為預處理階段進行了替換)
2>導致代碼可讀性差,可維護性差,容易誤用。
3>沒有類型安全的檢查 。
因此在c++中,可通過const來代替宏對常量的定義,用內聯函數來代替宏對函數的定義
內聯函數的優缺點:
https://mp.csdn.net/mdeditor/101083065#
概念:在C++中,auto作為一個新的類型指示符來定義變量,auto聲明的變量是由編譯器在編譯時期推導而得,變量被賦值什么類型,由初始化的值而定。
特性:
1>使用auto定義變量時必須對其進行初始化,在編譯階段編譯器需要根據初始化表達式來推導auto的實際類型。
2>auto并非是一種“類型”的聲明,而是一個類型聲明時的“占位符”,編譯器在編譯期會將auto替換為變量實際的類型
int TestAuto()
{
return 10;
}
int main()
{
int a = 10;
auto b = a;
auto c = 'a';
auto d = TestAuto();
cout << typeid(b).name() << endl; //int
cout << typeid(c).name() << endl; //char
cout << typeid(d).name() << endl; //int
//auto e; 無法通過編譯,使用auto定義變量時必須對其進行初始化
return 0;
}
使用方法
1>auto與指針和引用結合:用auto聲明指針類型時,用auto和auto* 沒有任何區別,但用auto聲明引用類型時則必須加&.
int x = 1;
auto px = &x;
auto *ppx = &x;
auto& rx = x;
auto rrx = x;
cout << typeid(px).name() << endl;
cout << typeid(ppx).name() << endl;
cout << typeid(rx).name() << endl;
cout << typeid(rrx).name() << endl;
rx = 3;
cout << x << endl; //x發生了變化說明是引用
rrx = 2;
cout << x << endl; //x未發生變化,說明不是引用
2>auto在同一行定義多個變量,當在同一行聲明多個變量時,這些變量必須是相同的類型,否則編譯器將會報錯,因為編譯器實際只對第一個類型進行推導,然后用推導出來的類型定義其他變量。
auto f = 1, g = 2;
//auto h = 1, i = 2.3; //編譯會報錯,h和i類型不同
3>auto不能直接用來聲明數組
int h[] = { 1, 2, 3 };
//auto t[] = { 4,5,6 };//編譯時會發生錯誤
為什么要引入這個概念?
對一個有范圍的集合由程序員來說明循環的范圍是多余的,有時候還會容易犯錯誤。因此C++11中引入了基于范圍的for循環。
用法:for循環后的括號由冒號“ :”分為兩部分:第一部分是范圍內用于迭代的變量,第二部分則表示被迭代的范圍。
int arr[] = { 1, 2, 3, 4, 5 };
for (auto& e : arr) //=>for (int i = 0; i < sizeof(array) / sizeof(array[0]); ++i)
e *= 2;
for (auto e : arr) //要對元素值進行改變,變量前要加&,不改變,直接普通變量
cout << e << " ";
對于數組而言,就是數組中第一個元素和最后一個元素的范圍;對于類而言,應該提供begin和end的方法,begin和end就是for循環迭代的范圍。
概念:nullptr指針空值常量,表示指針空值使用nullptr。
為什么要有nullptr,NULL為什么無法用于表示空指針了?
在指針定義時,要初始化(否則會出現野指針),在c中用NULL來給一個沒有指向的指針,但其實NULL是一個宏,在傳統的C頭文件(stddef.h)中,可以看到如下代碼
#ifndef NULL
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif
#endif
可以看到,NULL可能被定義為字面常量0,或者被定義為無類型指針(void*)的常量,所以在傳空指針時,會出現一些差強人意的錯誤,如下:
void f(int)
{
cout<<"f(int)"<<endl;
}
void f(int*)
{
cout<<"f(int*)"<<endl;
}
int main()
{
f(0);
f(NULL); //變成0了,進了第一個函數,但我們NULL想表示指針本是想進入第二個函數
f((int*)NULL);
return 0;
}
因而用nullptr來代替C中NULL在指針中的用法。
并且nullptr也是有類型的,其類型為nullptr_t,僅僅可以被隱式轉化為指針類型,nullptr_t被定義在頭文件中:typedef decltype(nullptr) nullptr_t;
注意:
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。