一篇文章帶你掌握C++虛函數(shù)的來(lái)龍去脈
一切從繼承講起
我們有一個(gè)基類 Animal。
有一個(gè) Dog 類繼承了 Animal。
有一個(gè) Fish 類也繼承了 Animal。
一切從上面的小例子開(kāi)始講起。
假設(shè) Animal 有一個(gè)成員函數(shù) print,可以打印自己是什么物種,在 Animal類中,可以這么寫(xiě):
class Animal
{
public:
void print()
{
std::cout << "我是 Animal" << std::endl;
}
};
class Dog: public Animal{};
class Fish: public Animal{};上面代碼里Dog和Fish沒(méi)有任何新的改動(dòng),僅僅繼承了Animal而已。所以當(dāng)實(shí)例化Dog或者Fish的時(shí)候,將生成的對(duì)象調(diào)用print函數(shù),只能顯示出"我是 Animal"。
Dog d; d.print(); // 打印 我是 Animal Fish f; f.print(); // 打印 我是 Animal
這樣不好,我們想要更精確的打印物種信息,所以我們?cè)谧宇愔兄囟xprint函數(shù):
class Dog: public Animal
{
public:
void print()
{
std::cout << "我是 Dog" << std::endl;
}
}
class Fish: public Animal
{
public:
void print()
{
std::cout << "我是 Fish" << std::endl;
}
}這樣的話,Dog類和Fish類的變量調(diào)用print函數(shù)的時(shí)候,就會(huì)打印相應(yīng)的信息了:
Dog d; d.print(); // 打印 我是 Dog Fish f; f.print(); // 打印 我是 Fish
到目前為止,一切都是順理成章。
繼承的語(yǔ)義是什么
請(qǐng)思考一下這個(gè)問(wèn)題:Dog 和 Animal 之間是什么關(guān)系?
在C++里,Dog繼承自Animal,我們就說(shuō),Dog就是Animal。
就是說(shuō),子類就是父類。
不是誰(shuí)包含誰(shuí)的關(guān)系。
這很重要,但是還是需要進(jìn)一步分析,【子類就是父類】這種關(guān)系到底在哪里能體現(xiàn)出來(lái)。
舉一個(gè)例子,我們有一個(gè)函數(shù),參數(shù)是 Animal*, 如下:
void foo(Animal* a)
{
}由于C++是一個(gè)強(qiáng)類型系統(tǒng),大部分語(yǔ)法都是來(lái)限制類型的。所以我們經(jīng)??梢詮暮瘮?shù)傳參來(lái)試圖理解一些比較難理解的概念,比如說(shuō)【子類就是父類】這個(gè)概念。
Animal a; Dog d; foo(&a); // 這個(gè)天經(jīng)地義,完美匹配類型系統(tǒng) foo(&d); // ????? 這個(gè)行不行呢
上面代碼最后一句到底行不行?
根據(jù)【子類就是父類】 -> 【Dog 就是 Animal】。答案很明顯,行!
我們?cè)賮?lái)看一個(gè)例子:
Animal a;
Animal* pa {&a}; // 依然天經(jīng)地義
Dog d;
Animal* pd {&d}; // 依然?????上面的代碼不是函數(shù)傳參,卻與函數(shù)傳參無(wú)二,花括號(hào)里需要填一個(gè)東西,來(lái)匹配前面的類型聲明。
很明顯,&d的類型是Dog*類型,完全可以當(dāng)做Animal*來(lái)使用。
小總結(jié),【子類就是父類】這個(gè)東西,在實(shí)踐里,就是說(shuō),當(dāng)我們需要一個(gè)【父類指針】的變量的時(shí)候,我們完全可以把一個(gè)【子類指針變量】丟進(jìn)去。
上面的總結(jié)不僅僅對(duì)于指針來(lái)說(shuō),對(duì)于引用也是同樣的。畢竟C++里,引用本身的概念與指針類似。
這里給個(gè)例子:
Dog d; Animal& rr2dfr82; // 完全可以
這是為什么呢,為什么可以這么做呢?
這是因?yàn)椋宇悓?duì)象的內(nèi)存里,確實(shí)包含了完整的基類對(duì)象。
注意,對(duì)象之間的關(guān)系可以說(shuō)包含與被包含了。
std::vector
我們?cè)谑褂?code>std::vector的時(shí)候,只能存儲(chǔ)同種類型的變量,比如說(shuō),我們要存的是Animal*類型的變量,根據(jù)上面的說(shuō)法,我們不僅僅能存Animal對(duì)象的指針,也可以存Dog對(duì)象 或者 Fish對(duì)象的指針。
這就給我們的代碼帶來(lái)了便利,一個(gè)std::vector可以來(lái)存儲(chǔ)所有Animal子類的指針了。
否則,我們需要給每一個(gè)子類聲明一個(gè)std::vector變量。
接著往下說(shuō),我們考慮下面的例子:
std::vector<Animal*> list;
Animal a;
Dog d;
Fish f;
list.push_back(&a);
list.push_back(&d);
list.push_back(&f);
for (auto e : list)
{
e->print();
}我們知道,這三個(gè)類,都有自己定義的print函數(shù),那么這個(gè)for循環(huán)執(zhí)行的時(shí)候,到底怎么打印呢?
我是 Animal
我是 Animal
我是 Animal
這種結(jié)果是出乎意料,還是不出所料呢,不同的人有不同的見(jiàn)解。
這里應(yīng)該是不出所料的,因?yàn)椋琧++是一個(gè)靜態(tài)類型的語(yǔ)言,大部分特性都是靜態(tài)的,所謂靜態(tài),就是編譯的時(shí)候就能確定一些事情,比如說(shuō),調(diào)用哪個(gè)函數(shù)。
由于e的類型是Animal*, 所以在編譯的時(shí)候,就已經(jīng)確定好了,for循環(huán)里的print是Animal::print。這就是所謂靜態(tài)。
我們發(fā)現(xiàn),這個(gè)std::vector確實(shí)能存儲(chǔ)Animal對(duì)象指針、 Dog對(duì)象指針、 Fish對(duì)象指針, 但好像一旦存儲(chǔ)進(jìn)去了,就無(wú)法區(qū)分,誰(shuí)是誰(shuí)了。
這怎么行,有一些行為,確實(shí)在子類里覆蓋了,比如說(shuō)print的行為。
如何讓靜態(tài)的c++編譯器生成一些看起來(lái)動(dòng)態(tài)的機(jī)器碼呢,比如說(shuō),上面的循環(huán)里,能夠調(diào)用各自類里面重新定義的print函數(shù),而不簡(jiǎn)單粗暴的直接使用Animal::print呢?
虛函數(shù)登場(chǎng)
虛函數(shù)定義
虛函數(shù)是一種特殊的類成員函數(shù), 這種函數(shù)在編譯器,無(wú)法確定真正的函數(shù)地址在哪里,所以稱之為虛函數(shù)。
程序運(yùn)行的時(shí)候,根據(jù)具體的對(duì)象是什么,就調(diào)用什么相應(yīng)的版本。
用嚴(yán)格一點(diǎn)的話來(lái)說(shuō):調(diào)用該虛函數(shù)出現(xiàn)的那個(gè)類和當(dāng)前對(duì)象的類,這兩個(gè)類之間,最靠下的那個(gè)版本的函數(shù)。
如何讓一個(gè)普通成員函數(shù)成為一個(gè)虛函數(shù)呢,在聲明的時(shí)候,前面加上virtual就行了。
話太繞了,我們來(lái)看例子:
class L1
{
};
class L2: public L1
{
public:
virtual void print()
{
std::cout << "L2" << std::endl;
}
};
class L3: public L2
{
}
class L4: public L3
{
public:
virtual void print()
{
std::cout << "L4" << std::endl;
}
}
///
void test()
{
L4 l4;
L1* pL1 {&l4};
pL1->print(); // 1. 打印什么
L2* pL2 {&l4};
pL2->print(); // 2. 打印什么
L3 l3;
pL2 = &l3;
pL2->print(); // 3. 打印什么
}我們來(lái)看上面的三個(gè)問(wèn)題.
問(wèn)題1:
pL1->print();。這句話其實(shí)很簡(jiǎn)單,壓根就不能編譯,因?yàn)閜L1的類型是L1*, 而L1類里面根本就沒(méi)有print函數(shù)。問(wèn)題2:
pL2->print();。L2*的身子裝了L4指針,這就很明顯了,L2和L4之間,最靠下的print,出現(xiàn)在L4中,所以這里應(yīng)該打印L4。問(wèn)題3:
pL2->print();。L2*的身子裝了L3指針,根據(jù)我們的說(shuō)法,也是很明顯的,L2和L3之間,最靠下的,還是L2,所以這里應(yīng)該打印L2。
通過(guò)這三個(gè)小問(wèn)題,應(yīng)該稍微了解虛函數(shù)到底調(diào)用哪一個(gè)的問(wèn)題了。
子類中如何改變一個(gè)虛函數(shù)的行為
如果想要在子類中改變一個(gè)虛函數(shù)的行為,那么就必須嚴(yán)格按照基類中該虛函數(shù)的函數(shù)簽名,重新實(shí)現(xiàn)這個(gè)虛函數(shù):
class A
{
public:
virtual void print(){}
}
class B: public A
{
public:
virtual void print(int a){}
}來(lái)看看上面的子類B中,我們給print加了一個(gè)參數(shù),此時(shí)B中的print還是A中的那個(gè)print嗎?
答案是否定的,
- 首先這個(gè)代碼是能編譯過(guò)的
- 只不過(guò),
B::print和A::print壓根就沒(méi)啥聯(lián)系,在具體的搜索虛函數(shù)進(jìn)行調(diào)用的時(shí)候,他們被看做完全不同的兩個(gè)函數(shù)。
再來(lái)看看虛函數(shù)的返回值類型所帶來(lái)的問(wèn)題:
class A
{
public:
virtual void print(){}
}
class B: public A
{
public:
virtual int print(){return 0;}
}問(wèn),此時(shí)B::print還是A::print嗎?
答案,是的。。。。只不過(guò),這個(gè)直接編譯不過(guò)。
編譯不過(guò)是好的,為什么,因?yàn)樵诰幾g的時(shí)候,就告訴你錯(cuò)在哪了。
上面那個(gè)由于疏忽或者別的原因,給原本的虛函數(shù)多加了一個(gè)參數(shù),這種才可怕呢,因?yàn)榫幾g通過(guò)了。
那怎么防范生成了一個(gè)新的函數(shù)?
override 限定符
如果在子類里面,我們確定要重新實(shí)現(xiàn)一個(gè)虛函數(shù),那么我們就在函數(shù)簽名的后面加上這個(gè)override限定符。
class A
{
public:
virtual void print(){};
};
class B: public A
{
public:
void print(int a) override {};
}看上面代碼,B這個(gè)子類中print函數(shù)前面前面,我們?nèi)サ袅?code>virtual, 而在花括號(hào)前面加了override。
此時(shí),編譯器就報(bào)錯(cuò)了,邏輯是這樣的:
- 編譯器看到
override,它就認(rèn)為print是從基類繼承而來(lái)的一個(gè)虛函數(shù),所以它去看看A::print, 發(fā)現(xiàn)這個(gè)函數(shù)沒(méi)有參數(shù)。 - 回過(guò)頭來(lái),發(fā)現(xiàn)
B::print(int)帶了一個(gè)參數(shù),編譯器直接報(bào)錯(cuò)。
這就讓錯(cuò)誤盡早出現(xiàn)在編譯時(shí)期,棒!
final 限定符
可能會(huì)有這么一種情況,有一個(gè)類A,里面有一個(gè)虛函數(shù)print,你寫(xiě)了一個(gè)類B,繼承了類A,然后override了這個(gè)print函數(shù)。然后別人寫(xiě)了一個(gè)類C繼承了類B,你不想類C擁有override這個(gè)print函數(shù)的權(quán)限。
此時(shí),在類B中,override print 函數(shù)的地方,可以加一個(gè)final:
class A
{
public:
virtual void print(){};
}
class B: public A
{
public:
void print() override final {}; // 注意看,加了final
}
class C: public B
{
public:
void print() override {}; // 編譯報(bào)錯(cuò)
}上面的代碼演示了,class C中無(wú)法繼續(xù)override print的寫(xiě)法。
還有一種極端的情況,你寫(xiě)了一個(gè)類A,你壓根就不想別人去繼承這個(gè)類A:
class A final
{
};
class B: public A // oh, 直接報(bào)錯(cuò)
{
};加了final之后,就可以阻止別的類來(lái)繼承了。
covariant 返回類型
上面講過(guò),一個(gè)虛函數(shù),想要在子類里override,那么函數(shù)簽名必須一模一樣,包括返回值類型。但是有一種特殊的情況,需要考慮??聪旅娴睦?/p>
class A
{
public:
void print()
{
std::cout << "This is A" << std::endl;
}
};
class B: public A
{
public:
void print()
{
std::cout << "This is B" << std::endl;
}
};
class L1
{
public:
virtual A* get()
{
return new A{};
}
};
class L2: public L1
{
public:
B* get() override
{
return new B{};
}
};我們先注意到,B和A就是兩個(gè)普通的有繼承關(guān)系的類,里面并沒(méi)有出現(xiàn)virtual函數(shù)。
真正要研究的是L2和L1,get 函數(shù)是一個(gè)virtual函數(shù),但是L2里get返回值類型是B*。
這似乎違反了virtual函數(shù)的規(guī)定,那就是函數(shù)簽名必須一致。
但是又能說(shuō)的通:【子類就是父類】。
所以上面的代碼能編譯過(guò)嗎?
答案是能。這種特殊的情況被稱之為covariant 返回類型,有的地方翻譯成協(xié)變返回類型。
接著看如下的代碼:
void test()
{
L2 l2;
l2.get()->print(); // 問(wèn)題1,這里打印什么?
L1& rl1{l2};
rl1.get()->print(); // 問(wèn)題2,這里打印什么?
}- 問(wèn)題1:這個(gè)地方不難,就是打印
This is B。 - 問(wèn)題2:我們來(lái)慢慢分析,rl1 聲明的類型是 L1& ,但是引用了一個(gè)子類對(duì)象l2。此時(shí)
rl2.get()是遵循虛函數(shù)的調(diào)用邏輯,也就是肯定調(diào)用的是L2::get。L2::get的返回類型是什么,是B*,所以直接得出結(jié)果應(yīng)該是B::print, 打印This is B。
不好意思,問(wèn)題2的結(jié)論是錯(cuò)的。
虛函數(shù)不會(huì)改變?cè)镜暮瘮?shù)返回類型,在L1這個(gè)基類中,返回類型就是A*,即使調(diào)用了L2::get,仍然返回了A*這個(gè)類型,如果你有IDE,你可以將鼠標(biāo)懸停在
rl1.get()->print();
get這個(gè)地方,會(huì)顯示出,返回類型是A*, 于是乎,最后的print其實(shí)是A::print, 所以打印了
This is A。
virtual destructor 虛析構(gòu)函數(shù)
在大部分時(shí)候,我們都無(wú)需為自定的class提供一個(gè)析構(gòu)函數(shù), 因?yàn)榇蟛糠謺r(shí)候自定義的class里面不包含需要釋放的資源,比如說(shuō)內(nèi)存,文件等等。此時(shí)c++會(huì)提供一個(gè)默認(rèn)的析構(gòu)函數(shù)。
但是,如果我們的class里有這種動(dòng)態(tài)的資源,那么就不得不提供一個(gè)自定義的析構(gòu)函數(shù),來(lái)針對(duì)這些動(dòng)態(tài)資源進(jìn)行釋放。
更進(jìn)一步的是,如果一個(gè)擁有動(dòng)態(tài)資源的class同時(shí)繼承了別的class,此時(shí)最好小心一點(diǎn):
這是啥意思, 來(lái)看例子:
class L1
{
public:
~L1()
{
std::cout << "L1 正在析構(gòu)" << std::endl;
}
};
class L2: public L1
{
int* resource;
public:
L2():resource{new int}
{
}
~L2()
{
delete resource;
}
};
void test()
{
L2* l2{new L2};
L1* pl1{l2};
delete pl1;
}分析以上代碼,pl1 指向了一個(gè)子類L2的對(duì)象,在delete pl1的時(shí)候,編譯器發(fā)現(xiàn),L1 的析構(gòu)函數(shù)是正常函數(shù),所以編譯器在這里指定決定調(diào)用L1::~L1這個(gè)函數(shù),然后就結(jié)束了。
我們會(huì)發(fā)現(xiàn),L2 的析構(gòu)函數(shù)并沒(méi)有被調(diào)用到,也就是說(shuō), resource 所指向的資源沒(méi)有被回收?。?!
怎么辦呢,將 L1 中的析構(gòu)函數(shù)標(biāo)記成virtual:
class L1
{
public:
virtual ~L1(){};
}這樣才能保證,任何繼承自L1的類中的動(dòng)態(tài)資源被回收。
結(jié)論:如果寫(xiě)了一個(gè)類,這個(gè)類有可能被別的類繼承的話,那么最好將這個(gè)類的析構(gòu)函數(shù)標(biāo)記成virtual的:
class A
{
public:
virtual ~A() = default;
}關(guān)于這一點(diǎn),有很多大師級(jí)人物都討論過(guò),不同的人有不同的看法,不過(guò),上面的結(jié)論還是穩(wěn)妥的,雖然有一點(diǎn)性能消耗。
虛函數(shù)如何實(shí)現(xiàn)的
為什么要有這個(gè)疑問(wèn),難道這種實(shí)現(xiàn)不正常嗎?
不正常,非常不正常,C++是一個(gè)靜態(tài)語(yǔ)言,必須先編譯再運(yùn)行,執(zhí)行什么函數(shù),一定是編譯時(shí)就決定好的。
而虛函數(shù)打破了這種既有的規(guī)則,而這種規(guī)則的打破依賴于函數(shù)指針。
下面來(lái)講講虛函數(shù)這一套邏輯到底是怎么跑起來(lái)的。
函數(shù)指針
這是一種指針,這個(gè)指針指向的是一塊代碼,用這個(gè)指針可以進(jìn)行函數(shù)調(diào)用:
void print_v1()
{
std::cout << "print_v1" <<std::endl;
}
void print_v2()
{
std::cout << "print_v2" <<std::endl;
}
void test()
{
auto f {print_v1};
f(); // 打印 print_v1
f = print_v2;
f(); // 打印 print_v2
}觀察上面的代碼,發(fā)現(xiàn),兩個(gè)f()調(diào)用了不同的函數(shù),這是一種動(dòng)態(tài)行為。也就是說(shuō),程序運(yùn)行的時(shí)候,根據(jù)f本身的指向,才能決定真正調(diào)用哪一塊代碼。
虛函數(shù)表
有了函數(shù)指針,使得動(dòng)態(tài)行為有了可能,剩下的就是奇思妙想,讓虛函數(shù)邏輯跑起來(lái)。
大部分編譯器采用了所謂虛函數(shù)表的東西來(lái)實(shí)現(xiàn)虛函數(shù)邏輯。
這種東西文字描述不清,直接看例子:
class L1
{
public:
virtual void func1()
{
}
virtual void func2()
{
}
};
class Sub1: public L1
{
public:
void func1() override
{
}
};
class Sub2: public L1
{
public:
void func2() override
{}
};先描述一下,上面有三個(gè)class,L1是一個(gè)基類,里面有兩個(gè)virtual 函數(shù):
- func1
- func2
然后
- Sub1繼承了L1, 然后override了 func1
- Sub2繼承了L1, 然后override了 func2
此時(shí),先來(lái)考慮一個(gè)小問(wèn)題,sizeof 三個(gè) class,應(yīng)該是多大呢,假如是64bit機(jī)器。
答案是都是占8字節(jié),也就是64bit。
那么這8字節(jié)存了啥東西?
答案就是,這8字節(jié)其實(shí)是一個(gè)指針,指向哪,先不說(shuō),一會(huì)再來(lái)說(shuō)明。
虛函數(shù)表的概念
對(duì)于上面的例子來(lái)說(shuō),編譯器生成了三個(gè)虛函數(shù)表,也就是L1、Sub1、Sub2每個(gè)class,各一個(gè)。
注意這個(gè)虛函數(shù)表是每個(gè)class一個(gè),而不是每個(gè)對(duì)象一個(gè),一定要搞明白。
這很類似于 class 里的靜態(tài)成員,這么說(shuō)就好理解了。
那虛函數(shù)表長(zhǎng)啥樣?
其實(shí)虛函數(shù)表就是一個(gè)數(shù)組,數(shù)組里的每一項(xiàng)就是一個(gè)簡(jiǎn)單的函數(shù)指針。
我們來(lái)畫(huà)一畫(huà)上面例子的虛函數(shù)表:

在右邊的代碼段里,我們可以看見(jiàn),一共有四個(gè)不同的函數(shù),這與我們的代碼是一致的。
再來(lái)看左邊的虛函數(shù)表,可以清晰的看出來(lái),每個(gè)類里的兩個(gè)虛函數(shù)都真實(shí)地指向了正確的版本。
光有這個(gè)虛函數(shù)表,是沒(méi)用的,在調(diào)用虛函數(shù)的地方,必須與這個(gè)虛函數(shù)表聯(lián)系起來(lái)。
還記得剛才說(shuō)的那個(gè)8字節(jié)的指針嗎。
那個(gè)指針就是起到這種關(guān)聯(lián)的。
我們看下面的例子:
void test()
{
L2 l2;
L1* p{&l2};
p->func1();
}我們畫(huà)出上面的整個(gè)關(guān)系圖:

此時(shí)用 p->func1() 的時(shí)候,為什么會(huì)調(diào)用到Sub1::func1就一目了然了,一直跟著指針往下走就明白了!
vtable指針
我們將上面的那個(gè)8字節(jié)指針?lè)Q做vtable指針,它的作用就是來(lái)指向相應(yīng)的class的虛函數(shù)表的。
一般而言,這個(gè)變量是在基類里聲明的,子類是繼承了這個(gè)變量。
在對(duì)象初始化的時(shí)候,這個(gè)指針會(huì)指向真正的本class的虛函數(shù)表。
比如說(shuō)
- Sub1對(duì)象里的vtable就會(huì)指向Sub1的虛函數(shù)表
- Sub2對(duì)象里的vtable就會(huì)指向Sub2的虛函數(shù)表
虛函數(shù)的消耗
我們從上面的實(shí)現(xiàn)可以看出,在使用虛函數(shù)的class里,強(qiáng)行塞入了一個(gè)vtable指針,占了8字節(jié),這無(wú)疑會(huì)增加內(nèi)存的消耗。
其次,調(diào)用虛函數(shù)的時(shí)候,需要三步走。
- 從vtable找到虛函數(shù)表
- 從虛函數(shù)表找到真正的函數(shù)指針
- 然后由函數(shù)指針找到函數(shù),進(jìn)行調(diào)用
而一般的函數(shù)只有最后一步,這無(wú)疑也是增加了一些步驟的,不過(guò)這種消耗不怎么明顯,所以該用虛函數(shù),還是盡量用吧,不要有什么心理負(fù)擔(dān),然后搞什么靜多態(tài)。
對(duì)了,我們把整個(gè)虛函數(shù)所進(jìn)行的行為稱之為多態(tài),這是一種動(dòng)態(tài)多態(tài),因?yàn)檫@是運(yùn)行時(shí)的行為。
至于什么叫靜多態(tài),那就不屬于本文所討論的了。
總結(jié)
到此這篇關(guān)于C++虛函數(shù)的文章就介紹到這了,更多相關(guān)掌握C++虛函數(shù)內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Visual Studio 2019配置OpenCV4.1.1詳細(xì)圖解教程
這篇文章主要介紹了Visual Studio 2019配置OpenCV4.1.1詳細(xì)圖解教程 ,需要的朋友可以參考下2020-02-02
C++數(shù)據(jù)精度問(wèn)題(對(duì)浮點(diǎn)數(shù)保存指定位小數(shù))
這篇文章主要介紹了對(duì)浮點(diǎn)數(shù)保存指定位小數(shù)。比如, 1.123456. 要保存1位小數(shù),,調(diào)用方法后, 保存的結(jié)果為: 1.1。 再比如,1.98765, 保存2位小數(shù)的結(jié)果為: 2.00,需要的朋友可以參考下2017-08-08
C語(yǔ)言實(shí)現(xiàn)經(jīng)典24點(diǎn)算法
這篇文章主要為大家詳細(xì)介紹了C語(yǔ)言實(shí)現(xiàn)經(jīng)典24點(diǎn)算法,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2019-10-10
C語(yǔ)言實(shí)現(xiàn)紙牌24點(diǎn)小游戲
這篇文章主要為大家詳細(xì)介紹了C語(yǔ)言實(shí)現(xiàn)紙牌24點(diǎn)小游戲,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2019-10-10
QT編寫(xiě)地圖實(shí)現(xiàn)離線輪廓圖的示例代碼
這篇文章主要介紹了在利用QT編寫(xiě)地圖時(shí)常常需要用到的離線輪廓圖,離線輪廓圖使用起來(lái)比線輪廓圖麻煩一點(diǎn),需要自己繪制。感興趣的小伙伴可以學(xué)習(xí)一下2021-12-12
C/C++通過(guò)IP獲取局域網(wǎng)網(wǎng)卡MAC地址
這篇文章主要為大家詳細(xì)介紹了C++如何通過(guò)Win32API函數(shù)SendARP從IP地址獲取局域網(wǎng)內(nèi)網(wǎng)卡的MAC地址,感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下2025-02-02
C語(yǔ)言中大小寫(xiě)字母相互轉(zhuǎn)化的方法示例
在C語(yǔ)言中,大小寫(xiě)字母的轉(zhuǎn)換可以通過(guò)標(biāo)準(zhǔn)庫(kù)中的ctype.h頭文件提供的函數(shù)來(lái)實(shí)現(xiàn),具體來(lái)說(shuō),toupper()函數(shù)可以將小寫(xiě)字母轉(zhuǎn)換為大寫(xiě)字母,而tolower()函數(shù)可以將大寫(xiě)字母轉(zhuǎn)換為小寫(xiě)字母,本文給大家介紹了C語(yǔ)言中大小寫(xiě)字母相互轉(zhuǎn)化的方法,需要的朋友可以參考下2024-08-08

