C++之C語法增強_寄存器_三目_引用初探


站在編譯器和C的角度剖析c++原理, 用代碼說話


Hello world

首先我們先引入無敵案例之hello world:

#include <iostream>
using namespace std;
int main(void) {
    std::cout << "Hello, World!\n";
    int i = 0;
    cin >> i;
    cout<<"你輸入的i是"<<i<<endl;
    return 0;
}

cout就是表示標準輸出終端, 但是這裏用了<<來表示,我們知道在C中這表示的是移位運算符,但是這裏卻表示輸出重定向?這就說明c++編譯器對這個符號做了增強的功能. 這個話題我們在之後的文章中會講到,這也是個重點.

面向對象VS面向過程

我們都知道c是面向過程的語言,c++是面向對象的語言, 那麼到什麼是面向過程什麼是面向對象呢,對於初學者來說的確很抽象,看到網上的一些概念更加混亂了,那麼我們用代碼說話:

//面向過程
int main(void){
    double r = 0;
    double s = 0;
    cout << "請你輸入圓的半徑";
    cin >> r;
    s = 3.14 * r * r;
    cout << "圓的面積是:" << s << endl;
    return 0;
}
//面向對象
struct Circle{
    double m_r;
    double m_s;
    void setR(double r){
        m_r = r;
    }
    double getS(){
        m_s = 3.14*m_r*m_r;
        return m_s;
    }
};
int main(void){

    Circle c1;
    double r = 0;
    cout << "請你輸入圓的半徑";
    cin >> r;
    c1.setR(r);
    cout << "圓的面積是" << c1.getS() << endl;
    return 0;
}

面向過程的代碼我就不解釋了, 面向對象的這個代碼中首先說的是定義Circle這個結構體數據類型,我們知道數據類型的本質是什麼?就是固定大小的內存塊的名字而已. 所以我們當然可以在c++中繼續使用struct, 但是在c++中struct相比於C的變化是能夠在結構體中定義方法,能夠在裏面繼承函數, 這也就是封裝的概念. 當然在後面更多的是使用class而不是struct.我們先循序漸進的給大家認識本質,讓大家看到c++其實不恐怖. c++中的struct和class還有一點不同是struct中定義的方法默認屬性是public, class中默認是private的,別急,這些後面都會涉及到,這裏只需要知道class很類似struct就行.
struct類型的加強:
C語言的struct定義了一組變量的集合,C編譯器並不認爲這是一種新的類型.
C++中的struct是一個新類型的定義聲明,進行了功能增強,內部還能加函數.
在c++的struct或class中的這些變量m_r叫做成員變量,void setR(double r){}這些方法叫做成員方法.
在main中,Circle c1;就是用類型定義變量,就是分配了個內存的意思. 只有執行c1.setR(r);c++編譯器纔會去調用函數.
我們這裏介紹個易犯錯誤模型:

class circle{
public:
    double r;
    double pi = 3.14;
    double area = pi * r * r;
};
int main01_04(void){
    circle p1;
    cout << "請輸入area" << endl;
    cin >> p1.r;
    cout << p1.area << endl;
    return 0;
}

這樣的結果是個亂碼,爲什麼呢?是不是你也錯了?這個錯誤知識點上面已經提到過了, 再分析一遍: circle p1;分配了內存,好,r因爲沒有初始化,所以r的內存中是亂碼, pi是3.14, 那麼現在area的內存中就是亂碼. 接着將終端輸入給r的內存賦值,好,這時候r的不是亂碼了,但是對area並無關係,所以還是亂碼. 怎麼處理呢?當然是像上面一樣在裏面寫方法來調用.

namespace

這裏先說一下每次寫c++都要打頭寫#include <iostream>, 我們知道這是爲了包含頭文件,但是爲啥不寫#include iostream.h呢?這是因爲當時設計者爲了和C有些大點的區別而做的一些並無卵用的活. 所以沒多大意義. 並且當我們使用#include <iostream>的時候如果沒有寫下面的using namespace std;就不能使用好多定義比如cout,你要想使用就得必須寫成std::cout這種形式,那這到底是爲什麼呢?那麼我我們看看iostream裏面到底是什麼鬼?

#ifndef _LIBCPP_IOSTREAM
#define _LIBCPP_IOSTREAM
/*
    iostream synopsis
#include <ios>
#include <streambuf>
#include <istream>
#include <ostream>

namespace std {

extern istream cin;
extern ostream cout;
extern ostream cerr;
extern ostream clog;
extern wistream wcin;
extern wostream wcout;
extern wostream wcerr;
extern wostream wclog;

}  // std

*/

#include <__config>
#include <ios>
#include <streambuf>
#include <istream>
#include <ostream>

#if !defined(_LIBCPP_HAS_NO_PRAGMA_SYSTEM_HEADER)
#pragma GCC system_header
#endif

_LIBCPP_BEGIN_NAMESPACE_STD

#ifndef _LIBCPP_HAS_NO_STDIN
extern _LIBCPP_FUNC_VIS istream cin;
extern _LIBCPP_FUNC_VIS wistream wcin;
#endif
#ifndef _LIBCPP_HAS_NO_STDOUT
extern _LIBCPP_FUNC_VIS ostream cout;
extern _LIBCPP_FUNC_VIS wostream wcout;
#endif
extern _LIBCPP_FUNC_VIS ostream cerr;
extern _LIBCPP_FUNC_VIS wostream wcerr;
extern _LIBCPP_FUNC_VIS ostream clog;
extern _LIBCPP_FUNC_VIS wostream wclog;

_LIBCPP_END_NAMESPACE_STD

#endif  // _LIBCPP_IOSTREAM

我們會發現它定義了好多cout, cin等等,但是它本身也沒有加using namespace std;這個語句,也就是說要想它本身定義的cout等用起來,就得配合using namespace std;. 好,那麼我們就說說這個namespace到底是什麼鬼?
namespace就是標準作用域, c++標準庫的所有定義都被扔在了一個叫std的namespace中. 在C++中,名稱(name)可以是符號常量、變量、宏、函數、結構、枚舉、類和對象等等。爲了避免在大規模程序的設計中,以及在程序員使用各種各樣的C++庫時,這些標識符的命名發生衝突,標準C++引入了關鍵字namespace(命名空間/名字空間/名稱空間/名域),可以更好地控制標識符的作用域。std是c++標準命名空間,c++標準程序庫中的所有標識符都被定義在std中,比如標準庫中的類iostream、vector等都定義在該命名空間中,使用時要加上using聲明(using namespace std) 或using指示(如std::string、std::vector<int>).C++命名空間的定義:namespace name { … }.
C++命名空間的使用:
使用整個命名空間:using namespace name;
使用命名空間中的變量:using name::variable;
使用默認命名空間中的變量:::variable;
默認情況下可以直接使用默 認命名空間中的所有標識符

namespace NameSpaceA
{
    int a = 0;
}
namespace NameSpaceB
{
    int a = 1;
    namespace NameSpaceC
    {
        struct Teacher
        {
            char name[10];
            int age;
        };   
    }
}
int main()
{
    using namespace NameSpaceA;
    using NameSpaceB::NameSpaceC::Teacher;  //::域作用符
    printf("a = %d\n", a);
    printf("a = %d\n", NameSpaceB::a);
    Teacher t1 = {"aaa", 3};
    printf("t1.name = %s\n", t1.name);
    printf("t1.age = %d\n", t1.age);
    system("pause");
    return 0;
}

c++之register關鍵字

register:這個關鍵字請求編譯器儘可能的將變量存在CPU內部寄存器中,而不是通過內存尋址訪問,以提高效率。注意是儘可能,不是絕對。你想想,一個CPU 的寄存器也就那麼幾個或幾十個,你要是定義了很多很多register 變量,它累死也可能不能全部把這些變量放入寄存器吧,輪也可能輪不到你.

int main()
{
    register int a = 0;
    printf("&a = %x\n", &a);
    system("pause");
    return 0;
}

早期C語言編譯器不會對代碼進行優化,因此register變量是一個很好的補充.
c++中的編譯器對register進行了功能增強, 即使沒有使用register修飾變量,c++也會自動優化常用的變量.

int main(void){
    register int i = 0;
    int b = 0;
    for (i = 0; i < 10000; i++) {
        ;
    }
    printf("%d \n", &i);
    return 0;
}

register關鍵字 請求編譯器讓變量i直接放在寄存器裏面,速度快, 在c語言中 register修飾的變量 不能取地址,但是在c++裏面做了內容. register關鍵字請求”編譯器”將局部變量存儲於寄存器中, C語言中無法取得register變量地址, 在C++中依然支持register關鍵字, C++編譯器有自己的優化方式,不使用register也可能做優化,會將經常使用的變量自動放在register中. C++中可以取得register變量的地址, 但是C++編譯器發現程序中需要取register變量的地址時,register對變量的聲明變得無效。
但是使用register修飾符有幾點限制:
首先,register變量必須是能被CPU所接受的類型。這通常意味着register變量必須是一個單個的值,並且長度應該小於或者等於整型的長度。不過,有些機器的寄存器也能存放浮點數.
其次,因爲register變量可能不存放在內存中,所以不能用”&”來獲取register變量的地址。由於寄存器的數量有限,而且某些寄存器只能接受特定類型的數據(如指針和浮點數),因此真正起作用的register修飾符的數目和類型都依賴於運行程序的機器,而任何多餘的register修飾符都將被編譯程序所忽略。
在某些情況下,把變量保存在寄存器中反而會降低程序的運行速度。因爲被佔用的寄存器不能再用於其它目的;或者變量被使用的次數不夠多,不足以裝入和存儲變量所帶來的額外開銷。
早期的C編譯程序不會把變量保存在寄存器中,除非你命令它這樣做,這時register修飾符是C語言的一種很有價值的補充。然而,隨着編譯程序設計技術的進步,在決定那些變量應該被存到寄存器中時,現在的C編譯環境能比程序員做出更好的決定。實際上,許多編譯程序都會忽略register修飾符,因爲儘管它完全合法,但它僅僅是暗示而不是命令。

c++之boolean運算

C++在C語言的基本類型系統之上增加了bool, C++中的bool可取的值只有true和false, 理論上bool只佔用一個字節,如果多個bool變量定義在一起,可能會各佔一個bit,這取決於編譯器的實現. true代表真值,編譯器內部用1來表示, false代表非真值,編譯器內部用0來表示. bool類型只有true(非0)和false(0)兩個值. C++編譯器會在賦值時將非0值轉換爲true,0值轉換爲false.

int main(void){
    bool b1;
    b1 = 4;
    printf("b1 = %d\n", b1);//1
    b1 = -1;
    printf("b1 = %d\n", b1);//1
    b1 = 0;
    printf("b1 = %d\n", b1);
    int a;
    bool b = true;
    printf("b = %d, sizeof(b) = %d\n", b, sizeof(b));//1, 1
    b = 4;
    a = b;
    printf("a = %d, b = %d\n", a, b);//1, 1
    b = -4;
    a = b;
    printf("a = %d, b = %d\n", a, b);//1, 1
    a = 10;
    b = a;
    printf("a = %d, b = %d\n", a, b);//10, 1
    b = 0;
    printf("b = %d\n", b);//0
    return 0;
}

c++之三目運算

首先我們考慮C中的三目運算符:

int main()
{
    int a = 10;
    int b = 20;
    (a < b ? a : b )= 30;
    printf("a = %d, b = %d\n", a, b);
    system("pause");
    return 0;
}

這個在C編譯器是報錯的,三目運算符是一個表達式 ,表達式不可能做左值. (a < b ? a : b )是一個表達式, 表達式的值在寄存器中,寄存器中是沒有地址的,給一個沒有地址的存儲區賦值肯定gg. 但是在c++中可以, 說明c++編譯器進行了功能增強, 返回的是a本身. 元素當左值的必要條件就是,元素有地址空間.那麼我們就能這樣的改這個代碼在C中也能實現左值功能:
*(a < b ? &a : &b) = 30;, 所以說C++到底幹了個啥,其實就是幹了這個.
比如說(a < b ? 100 : b)=30這就錯了,100的地址是什麼鬼?所以說三目運算符可能返回的值中如果有一個是常量值,則不能作爲左值使用,說白了就是沒地址, 迴歸之前提到的元素當左值的必要條件就是,元素有地址空間.
總結一下:
C語言中的三目運算符返回的是變量值,不能作爲左值使用, C++中的三目運算符可直接返回變量本身,因此可以出現在程序的任何地方,拋磚引出了我們下面討論以及下一篇文章主要討論的重點—引用.

c++之const

const在C中其實是個”冒牌貨”,讓我們開一段C:

int main()
{
    const int a = 10;
    int *p = (int*)&a; 
    printf("a===>%d\n", a);
    *p = 11;
    printf("*p===>%d\n", *p);//11
    printf("a===>%d\n", a);//11
    return 0;
}

在C中雖然用const定義了a, 也就是說我們不能直接去修改a的值,a變成了只讀的, 但是我們通過指針p指向a的地址,然後間接的去修改a的地址指向的內存空間, 那麼a的值也就變了.是不是感覺被騙了的不爽. 但是在c++中上面的這段代碼就是a返回的依然是10. 這是爲什麼呢?首先能分析出來的是*p所指的內存空間和&a不一樣. 這裏解釋一下c++編譯器對const做了什麼手腳:
C語言中的const變量, C語言中const變量是隻讀變量,有自己的存儲空間. 然後在c++中, 一旦用const神明內部就維護了一個符號表, a->10. 這個也是隻讀的. 編譯過程中若發現使用常量則直接以符號表中的值替換, 編譯過程中若發現對const使用了extern或者&操作符,則給對應的常量分配存儲空間(兼容C), 也就是說C++中的const常量, 並不會給分配空間而是僅僅保存在了符號表中, 只有在&或extern的時候,纔會給分配個空間供你隨便玩,但是這個空間是不會對之前符號表中的值造成任何影響的. c++中的const修飾的,是一個真正的常量,編譯期間就定下來了, 而不是C中變量(只讀). 那就有一個問題,這豈不是和宏定義一樣了,的確是這樣,它們很想,但是也是有不同的:
c++中的const常量類似於宏定義const int c = 5; ≈ #define c 5, C++中的const常量在與宏定義不同, const常量是由編譯器處理的,提供類型檢查和作用域檢查, 宏定義由預處理器處理,單純的文本替換.

void fun1()
{
    #define a 10//從這行往下所有的代碼都能使用a
    const int b = 20;//只有在作用域內能使用
    //#undef//但是也是可以卸載掉
}
void fun2()
{
    printf("a = %d\n", a);
    //printf("b = %d\n", b);
}

const int a = 1;int const b = 1;是一樣的.

char buf[20];
getMem(getMem(&buf));

這種情況下C語言是通過的,但是在c++中是不通過的. 本身這個語句就是很危險的, 因爲這裏其實char **char (*)[20]是不一樣的. char (*)[20]是指向數組的指針, 你一旦寫buf = buf + 1; 然後getMem(&buf)就gg. 所以千萬不要將buf地址直接放進去了,一般都是定義一個指針char *p = NULL. 然後再傳入p的地址在函數中分配.
還有是區分一些:const Teacher *p, Teacher *const p:
Teacher *const p:表示常指針,表示這個指針只能指向一個地方,也就是p不能再賦值其他的地址.
const Teacher *p:表示指向常量的指針, 也就是說這個指針指向的常量不能再賦值其他. *p不能再進行賦值.

c++之引用初探

變量名實質上是一段連續存儲空間的別名,是一個標號(門牌號), 程序中通過變量來申請並命名內存空間, 通過變量的名字可以使用存儲空間. 那麼對一段連續的內存空間只能取一個別名嗎?在c++中當然不是的.
引用是C++的概念,屬於C++編譯器對C的擴展.引用基於C的本質就是它是個常指針.

nt main()
{
    int a = 0;
    int &b = a; //== int * const b = &a 
     b = 11;  //== *b = 11;
    return 0;
}

請不要用C的語法考慮, 引用可以看作一個已定義變量的別名, 引用作爲函數參數聲明時不進行初始化.

nt main()
{
    int a = 10;
    int &b = a;
    //b是a的別名,請問c++編譯器後面做了什麼工作?
    b = 11;
    cout<<"b--->"<<a<<endl;
    printf("a:%d\n", a);
    printf("b:%d\n", b);
    printf("&a:%d\n", &a);
    printf("&b:%d\n", &b);//請思考:對同一內存空間可以取好幾個名字嗎?
    system("pause");
    return 0;
}

普通引用在聲明時必須用其它的變量進行初始化, 爲什麼要初始化呢,就是因爲上面提到的引用的本質就是常指針,const定義的能不進行初始化嗎?
引用作爲其它變量的別名而存在,因此在一些場合可以代替指針, 引用相對於指針來說具有更好的可讀性和實用性.

int swap1(int &a, int &b){
    int tmp = a;
    a = b;
    b = tmp;
    return 0;
}
int swap2(int *a, int *b){
    int tmp = *a;
    *a = *b;
    *b = tmp;
    return 0;
}

那麼普通的引用有自己獨立的空間嗎?當然是有的,讓我們敲一下這段代碼:

struct Teacher {
    int &a;
    int &b;
};
int main()
{
    printf("sizeof(Teacher) %d\n", sizeof(Teacher));//8
    return 0;
}

有人在這裏會問爲什麼是8,爲什麼不需要初始化?不是說需要初始化嗎?首先這是常指針是個指針,所以8就知道了吧,初始化的問題,結構體的定義,只能告訴編譯器,裏面有什麼類型的數據,佔有多少字節,是不能在裏面賦值的。構造函數是類定義裏的,在那裏面是可賦初值的. 也就是說參數列表,類成員中是可以不初始化話的,在調用函數, 構造對象時纔算初始化.
從使用的角度,引用會讓人誤會其只是一個別名,沒有自己的存儲空間。這是C++爲了實用性而做出的細節隱藏.
C中間接賦值的三個條件:
1. 定義兩個變量 (一個實參一個形參)
2. 建立關聯 實參取地址傳給形參
3. *p形參去間接的修改實參的值.
那麼引用就是將後兩步c++編譯器幫我們手動取了一個實參地址, 傳給了形參引用(常指針).

struct Teacher{
    char name[64];
    int age;
};
//以前我們這樣寫
void printTe(Teacher *p){
    printf("%d\n", p->age);
}
//現在我們用引用了
void printTe(Teacher &t2){//t2就是t1的別名,不存在深淺拷貝問題
    printf("%d\n", t2.age);
}
int main(void){
    int a = 10;//c編譯器分配四個空間的內存,能不能在這塊內存給a再取一個別名
    //引用的語法:Type& name = var;
    int &b = a;//表示b就是a的別名
    a = 11;//直接賦值
    {
        int  *p = &a;
        *p = 12;//C中的間接賦值
    }
    b = 14;//這樣b和a都是14了

    Teacher t1;
    Teacher &t2 = t1;
    t1.age = 10;
    printTe(&t1);
    printTe(t1);
}

容我給你分析一下引用的過程: 當Teacher &t2 = t1;時,進行的內部操作就是Teacher * const t1 = &t1., 然後執行printTe(t1);的時候, 調到了函數參數列表那裏,void printTe(Teacher &t2){C++編譯器發現呀?是個引用類型,那我給你轉一下: Teacher * const t2. 所以printTe(t1);這一步就等價於printTe(&t1)過來了. 然後在函數內部編譯器又自動給我們取地址間接改變了值. (注意常指針是可以取改變指針指向的內存空間的值的).
引用一旦生出來,就必須給它賦個值, 就類似於buffer, char buf[20];一出現就是個常指針(注意與常量指針是不同的).初值就是內存首地址.


聯繫方式: [email protected]

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章