不知道为何,彷佛不少人理解跑偏了,在这里我要说明一下。ios
首先,我并无对C++语言有偏见,我只是单纯的在学习时,在理解时,对C++语言进行一些吐槽,我相信,不少学习C++的人,也会有相似的吐槽。c++
其次,我吐槽了我之前的一些C++同事,这与其余C++开发无关,若是你感同身受,那说明你要检讨一下了。git
前言程序员
这是一篇C#开发从新学习C++的体验文章。github
做为一个C#开发为何要从新学习C++呢?由于在C#在不少业务场景须要调用一些C++编写的COM组件,若是不了解C++,那么,很容易。。。注定是要被C++同事忽悠的。编程
我在和不少C++开发者沟通的时候,发现他们都有一个很是奇怪的特色,都很爱装X,都以为本身技术很好,还很爱瞧不起人;但若是多交流,会发现更奇怪的问题,他们几乎都不懂代码设计,面向对象和业务逻辑的代码写的也都很烂。设计模式
因此,此次重温C++也是想了解下这种奇异现象的缘由。编程语言
C++重温函数
首先打开VisualStudio,建立一个C++的Windows控制台应用程序,以下图:学习
图中有四个文件,系统默认为我打开了头文件和源文件的文件夹。
系统这么作是有意义的,由于刚学习时,外部依赖项,能够暂时不用看,而资源文件夹是空的,因此咱们只专一这两个文件夹就能够了。
做为一个C#开发,我对C++就是只知其一;不知其二,上学学过的知识也都忘记的差很少了,不过,我知道程序入口是main函数,因此我在项目里先找拥有main函数的文件。
结果发现ConsoleTest.cpp 文件里有main函数,那么,我就在这个文件里开始学习C++了,并且它的命名和我项目名也同样,因此很肯定,它就是系统为我建立的项目入口文件。
而后我打开ConsoleTest.cpp 文件,定义一个字符串hello world,准备在控制台输出一下,结果发现编译器报错。。。只好调查一下了。
调查后得知,原来,c++里没有string类型,想使用string类型,只能先引用string的头文件,在引用命名空间std,以下:
#include "pch.h"
#include <string>
using namespace std;
int main()
{
string str = "Hello World!\n";
}
头文件
头文件究竟是什么呢?
头文件,简单来讲就是一部分写在main函数上面的代码。
好比上面的代码,咱们将其中的引用头文件和使用命名空间的代码提取出来,写进pch.h头文件;而后,咱们获得代码以下图:
pch.h头文件:
ConsoleTest.cpp文件:
也就是说,头文件是用来提取.cpp文件的代码的。
呃。。。好像头文件很鸡肋啊,一个文件的代码为何要提取一部分公共的?写一块儿不就行了!为何要搞个文件来单独作,多傻的行为啊!
好吧,一开始我也的确是这么想的。
后来我发现,头文件,原来并非单纯的提取代码,仍是跨文件调用的基础。
也就是说,ConsoleTest.cpp文件,想调用其余Cpp文件的变量,必须经过头文件来调用。
好比,我新建一个test.cpp和一个test.h文件。
而后我在test.cpp中,定义变量test=100;以下:
#include "pch.h" #include "test.h" int test = 100;
接着我在test.h文件中再声明下test变量,并标记该变量为外部变量,以下。
extern int test;
如今,我在回到ConsoleTest.cpp文件,引用test.h文件;而后我就能够在ConsoleTest.cpp文件中使用test.cpp中定义的test变量了,以下:
#include "pch.h" #include "test.h" int main() { string str = "Hello World!\n"; cout << test << endl; }
如上述代码所示,咱们成功的输出了test变量,其值为100。
到此,咱们应该了解到了,头文件的主要做用应该是把被拆散的代码,扭到一块儿的纽带。
----------------------------------------------------------------------------------------------------
PS:我在上面引用字符串头文件时,使用的引用方法是【#include <string>】;我发现,引用该头文件时,并无加后缀.h;我把后缀.h加上后【#include <string.h>】,发现编译依然能够经过。
简单的调查后得知,【#include <string>】是C++的语法,【#include <string.h>】是语法。由于C++要包含全部C的语法,因此,该写法也支持。
Cin与Cout
Cin与Cout是控制台的输入和输出函数,我在测试时发现,使用Cin与Cout须要引用iostream头文件【#include <iostream>】,同时也要使用命名空间std。
#include <iostream> using namespace std;
在上面,咱们提到过,使用字符串类型string时,须要引用头文件string.h和使用命名空间std,那么如今使用Cout也要使用命名空间std。这是为何呢?
只能推断,两个头文件string.h和iostream.h在定义时,都定义在命名空间std下了。并且,经过我后期使用,发现还有好多类和类型也定义在std下了。
对此,我只能说,好麻烦。。。首先,缺失基础类型这种事,就很奇怪,其次不是一个头文件的东西,定义到一个命名空间下,也容易让人混乱。
不过,对于C++,这么作好像已是最优解了。
----------------------------------------------------------------------------------------------------
PS:Cin与Cout是控制台的输入和输出函数,开始时,我也不太明白,为何使用这样两个不是单词的东西来做为输入输出,后来,在调查资料时,才明白,原来这个俩名字要拆开来读。
读法应该是这样的C&in和C&out,这样咱们就清晰明白的理解了该函数了。
define,typedef,指针,引用类型,const
define
首先说define,define在C++里好像叫作宏。就定义一个全局的字符串,而后再任何地方均可以替换,以下:
#include "pch.h" #include "test.h" #define ERROR 518 int defineTest() { return ERROR; } int main() { cout << defineTest() << endl; }
也就是说,define定义的宏,在C++里就是个【行走的字符串】,在编译时,该字符串会被替换回最初定义的值。这。。。这简直就是编译器容许的bug。。。
不过,它固然也有好处,就是字符串更容易记忆和理解。可是说实话,定义一个枚举同样好记忆,并且适用场景更加丰富,因此,我的感受这个功能是有点鸡肋,不过C++好多代码都使用了宏,因此仍是须要了解起来。
typedef
typedef是一个别名定义器,用来给复杂的声明,定义成简洁的声明。
struct kiba_Org { int id; }; typedef struct kiba_new { int id; } kiba; int main() { struct kiba_Org korg; korg.id = 518; kiba knew; knew.id = 520; cout << korg.id << endl; cout << knew.id << endl; }
如上述代码所示,我定义了一个结构体kiba_Org,若是我要用kiba_Org声明一个变量,我须要这样写【struct kiba_Org korg】,必须多写一个struct。
但我若是用typedef给【struct kiba_Org korg】定义一个别名kiba,那么我就能够直接拿kiba声明变量了。
呃。。。对此,我只能说,为何会这么麻烦!!!
觉得这就很麻烦了吗?NO!!!还有更麻烦的。
好比,我想在我定义的结构体里使用自身的类型,要怎么定义呢?
由于在C++里,变量定义必须按照先声明后使用的【绝对顺序】,那么,在定义时就使用自身类型,编译器会提示错误。
若是想要让编译器经过,就必须在使用前,先给自身类型定义个别名,这样就能够在定义时使用自身类型了。
呃。。。好像有点绕,咱们直接看代码。
typedef struct kibaSelf *kibaSelfCopy; struct kibaSelf { int id; kibaSelfCopy myself; }; int main() { kibaSelf ks; ks.id = 518; kibaSelf myself; myself.id = 520; ks.myself = &myself; cout << ks.id << endl; cout << ks.myself->id << endl; }
如上述代码所示,咱们在定义结构体以前,先给它定义了个别名。
那么,变量定义不是必须按照先声明后使用的【绝对顺序】吗?为何这里,又在定义前,能够定义别名了呢?这不是矛盾了吗?
不知道,反正,C++就是这样。。。就这么屌。。。
指针
指针在C++中,就是在变量前加个*号,下面咱们定义个指针来看看。
int i = 518; int *ipointer = &i; int* ipointer2 = &i; cout << "*ipointer" << *ipointer << "===ipointer" << ipointer << endl;
如上述代码所示,咱们定义了俩指针,int *ipointer 和int* ipointer2。能够看到,我这俩指针的*一个靠近变量一个靠近声明符int,但两种写法都正确,编译器能够编译经过。
呃。。。就是这么屌,学起来就是这么优雅。。。
接着,咱们用取地址符号&,取出i变量的地址给指针,而后指针变量*ipointer中ipointer存储的是i的地址,而*ipointer存储的是518,以下图:
那么,咱们明明是把i的地址给了变量*ipointer,为何*ipointer存储的是518呢?
由于。。。就是这么屌。。。
哈哈,不开玩笑了,咱们先看这样一段代码,就能够理解了。
int i = 518; int *ipointer; int* ipointer2; ipointer = &i; ipointer2 = &i; cout << "*ipointer" << *ipointer << "===ipointer" << ipointer << endl;
如上述代码所示,我把声明和赋值给分开了,这样就形象和清晰了。
咱们把i的地址给了指针(*ipointer)中的ipointer,因此ipointer存的就是i的地址,而*ipointer则是根据ipointer所存储的地址找到对应的值。
那么,int *ipointer = &i;这样赋值是什么鬼?这应该报错啊,应该不容许把i的地址给*ipointer啊。
呃。。。仍是那句话,就是这么屌。。。
->
->这个符号大概是指针专用的。下面咱们来看这样一段代码来了解->。
kiba kinstance; kiba *kpointer; kpointer = &kinstance; (*kpointer).id = 518; kpointer->id = 518; //*kpointer->id = 518;
首先咱们定义一个kiba结构体的实例,定义定义一个kiba结构体的指针,并把kinstance的地址给该指针。
此时,若是我想为结构体kiba中的字段id赋值,就须要这样写【(*kpointer).id = 518】。
我必须把*kpointer扩起来,才能点出它对应的字段id,若是不扩起来编译器会报错。
这样很麻烦,没错,按说,微软应该在编译器中解决这个问题,让他*kpointer不用被扩起来就可使用。
但很显然,微软没这样解决,编译器给的答案是,咱们省略写*号,而后直接用存储地址的kpointer来调用字段,但调用字段时,就不能再用点(.)了,而是改用->。
呃。。。解决的就是这么优雅。。。没毛病。。。
引用类型
咱们先定义接受引用类型的函数,以下。
int usage(int &i) { i = 518; return i; } int main() { int u = 100; usage(u); cout << "u" << u << endl; }
如上述代码所示,u通过函数usage后,他的值被改变了。
若是咱们删除usage函数中变量i前面的&,那么u的值就不会改变。
好了,那么&符号不是咱们刚才讲的取地址吗?怎么到这里又变成了引用符了呢?
仍是那句话。。。就是这么屌。。。
呃。。。还有更屌的。。。咱们来引用个指针。
void usagePointer(kiba *&k, kiba &kiunew) { k = &kiunew; k->id = 518; } int main() { kiba kiunew; kiba kiu; kiba *kiupointer; kiupointer = &kiu; kiupointer->id = 100; kiunew.id = 101; cout << "kiupointer->id" << kiupointer->id << "===kiupointer" << kiupointer << endl; usagePointer(kiupointer, kiunew); cout << "kiupointer->id" << kiupointer->id << "===kiupointer" << kiupointer << endl; }
如上述代码所示,我定义了两个结构体变量kiunew,kiu,和一个指针*kiupointer,而后我把kiu的地址赋值给指针。
接着我把指针和kiunew一块儿发送给函数usagePointer,在函数里,我把指针的地址改为了kiunew的地址。
运行结果以下图。
能够看到,指针地址已经改变了。
若是我删除掉函数usagePointer中的【引用符&】(某些状况下也叫取地址符)。咱们将获得以下结果。
咱们从图中发现,不只地址没改变,赋值也失败了。
也就是说,若是咱们不使用【引用符&】来传递指针,那么指针就是只读的,没法修改。
另外,你们应该也注意到了,指针的引用传递时,【引用符&】是在*和变量之间的,若是*&k。而普通变量的引用类型传递时,【引用符&】是在变量前的,如&i。
呃。。。指针,就是这么屌。。。
const
const是定义常量的,这里就很少说了。下面说一下,在函数中使用const符号。。。没错,你没看错,就是在函数中使用const符号。
int constusage(const int i) { return i; }
如代码所示,咱们在入参int i前面加上了const修饰,而后,咱们获得这样的效果。
i在函数constusage,没法被修改,一但赋值就报错。
呃。。。基于C#,估计确定很差理解这个const存在的意义了,由于若是不想改,就别改啊,标只读这么费劲干什么。。。
不过咱们换位思考一下,C++中这么多内存控制,确实很乱,有些时候加上const修饰,标记只读,仍是颇有必要的。
PCH
在项目建立的时候,系统为咱们建立了一个pch.h头文件,而且,每一个.cpp文件都引用了这个头文件【#include "pch.h"】。
打开.pch发现,里面是空代码,在等待咱们填写。
既然.pch没有被使用,那么将【#include "pch.h"】删掉来简化代码,删除后,发现编译器报错了。
调查后发现,原来项目在建立的时候,为咱们设置了一个属性,以下图。
如图,系统咱们建立的pch.h头文件,被设置成了预编辑头文件。
下面,我修改【预编译头】属性,修改成不使用预编译头,而后咱们再删除【#include "pch.h"】引用,编译器就不会报错了。
那么,为何建立文件时,会给咱们设置一个预编译头呢?微软这么作确定是有目的。
咱们经过名字,字面推测一下。
pch.h是预编译头,那么它的对应英文,大概就是Precompile Header。即然叫作预编译,那应该在正式编译前,执行的编译。
也就是,编译时,文件被分批编译了,pch.h预编译头会被提早编译,咱们能够推断,预编译头是用于提升编译速度的。
类
C++是一个同时面向过程和面向对象的编程语言,因此,C++里也有类和对象的存在。
类的基础定义就很少说了,都同样。
不过在C++中,由于,引用困难的缘由(上面已经描述了,只能引用其余.cpp文件对应的头文件,而且,.cpp实现的变量,还得在头文件里外部声明一下),因此类的定义写法也发生了改变。
C++中建立类,须要在头文件中声明函数,而后在.cpp文件中,作函数实现。
可是这样作,明显是跨文件声明类了,但C++中又没有相似partial关键字让俩个文件合并编译,那么怎么办呢?
微软给出的解决方案是,在.Cpp文件中提供一个类外部编写函数的方法。
下面,咱们简单的建立一个类,在头文件中声明一些函数和一些外部变量,而后在.cpp文件中实现这些函数和变量。
右键头文件文件夹—>添加——>类,在类名处输入classtest,以下图。
而后咱们会发现,系统为咱们建立了俩文件,一个.h头文件和一个.cpp文件,以下图。
而后编写代码以下:
classtest.h头文件:
class classtest { public: int id; string name; classtest(); ~classtest(); int excute(int id); private: int number; int dosomething(); };
calsstest.cpp文件:
#include "pch.h" #include "classtest.h" classtest::classtest() { } classtest::~classtest() { } int classtest::excute(int id) { this->id = id; return this->id; } int classtest::dosomething() { this->number = 520; return this->number; }
调用测试代码以下:
#include "pch.h" #include "classtest.h" int main() { classtest ct; ct.excute(518); classtest *ctPointer = new classtest; ctPointer->excute(520); cout << "ct.id" << ct.id << "===ctPointer" << ctPointer->id << endl; }
结语
经过重温,我得出以下结论。
一,C++并非一门优雅的开发语言,他自身存在很是多的设定矛盾和混淆内容,所以,C++的学习和应用的难度远大于C# ;其难学的缘由是C++自己缺陷致使,而不是C++多么难学。
二,指针是C++开发学习设计模式的拦路虎,用C++学习那传说中的26种设计模式,还勉强能够;但,若是想学习MVVM,AOP等等这些的设计模式的话,C++的指针会让C++开发付出更多的代码量,所以多数C++开发对设计模式理解水平很低也是能够理解的了。事实上,C++开发是很难出高级程序员,大部分都被困在中级程序员这个水平线上了。
三,经过学习和反思,发现,我曾经接触的那些爱装X的C++开发,确实是坐井观天、夜郎自大,他们的编写代码的思惟逻辑,确确实实是被C++的缺陷给限制住了。
----------------------------------------------------------------------------------------------------
到此,我重温C++的心路历程就结束了。
代码已经传到Github上了,欢迎你们下载。
Github地址:https://github.com/kiba518/C-ConsoleTest
----------------------------------------------------------------------------------------------------
注:此文章为原创,欢迎转载,请在文章页面明显位置给出此文连接!
若您以为这篇文章还不错,请点击下方的【推荐】,很是感谢!