C++自学笔记v1.0:语法与编译过程

一、Dev C/C++编译器gcc/g++

1.1 常见编译参数与相关文件

参数名描述
-c仅编译,不链接
-o输出文件名
-l链接库文件名,动态库(.so(linux)/.dll(win))、静态库(.a),仅用于链接时寻找库,而可执行文件寻找动态库仍然依据系统环境变量。
-L链接库所在路径文件夹
-I头文件所在文件夹
名称描述
.dll动态库(dynamic linked library)文件
.h/.hpp头文件,由于预编译时被“#include”指令包含

1.2 编译过程概述

程序编译过程

1.预编译器执行#include、#define等编译指令对源文件进行预处理,include指令的作用等同于jsp:include,即文本替换包含进源文件。

如果一个文件中包含许多头文件,编译器为了优化编译速度,会预编译头文件为.h.gch文件,下次将会加载.h.gch文件。当修改.h文件时需要删除.h.gch文件。

2.编译器将源文件(.cpp/.c)编译为中间文件(.o);

gcc/g++ -c source.cpp/.c

3.编译器将多个.o文件链接为生成可执行文件,window下是exe;

gcc/g++ tmp1.o tmp2.o -o target.exe

4.若没有链接其他源文件,即没有使用其他文件中的定义,可以直接一步到位;

gcc/g++ source.cpp -o target.exe

5.在链接的多个中间文件(.o)中,不能对函数进行重复定义,故而一般不会在共享头文件中定义函数,而只是声明函数原型,因为头文件包含不同于java的import,是文本替换;

6.链接过程分为静态链接和动态链接;

(1)静态链接: 即把链接的东西包含在生成的可执行文件中,删除链接后的静态链接库,如.a,不会影响可执行文件运行,但体积相对较大,不过运行相对较快.

(2)动态链接: 是在执行的过程中再去寻找动态链接库,如.so/.dll,若缺失动态链接库,会报错,但可执行文件较小,但运行相对较慢,动态链接库更类似于java的类库使用,多个文件共享动态库时只是获得了一个函数的相同引用,而静态库是将函数一份份拷贝,因此除了持久化储存空间占用多,运行内存占用也多但如果头文件不在一个项目中多个源文件中使用,可以定义函数在头文件中。声明是可以多个链接文件相互重复的,定义一般不行。

7.还可以将多个源文件(.cpp/.c)一起编译成一个可执行文件或.o文件。

gcc/g++ source.cpp source1.cpp -o target.exe

8.C++编译器允许include后的源文件存在重复的声明,但不允许重复的定义。如声明void hello();void hello;两个hello函数,但不允许class myc {}; class myc {}重复定义两个myc类。

9.链接器生成的可执行文件需要于自定义动态链接库放置于相同目录下,除非修改系统环境变量。当然修改环境变量也可以由exe程序本身实现

10.CPLUS_INCLUDE_PATH环境变量配置第三方Include的头文件目录,也可以用-I参数在g++编译时指定,C_INCLUDE_PATH环境变量则指定gcc编译器的include目录

11.LIBRARY_PATH环境变量配置g++/gcc编译时对应的共享库目录,也可以通过-L参数指定

二、 cl编译器,Visual C++

微软提供的编译环境,适合Windows开发。

2.1 相关文件

名称描述
.lib静态库文件,链接时被包含进exe可执行文件,除了作为静态库自身作用,也可以作为动态库DLL的指引(见下文)。
g++编译器可以直接通过-l指令指定动态库编译。
.dll动态库(dynamic linked library)文件
.pdb数据库文件,保存一些编译信息,常用于开发时Debug,VS中在调试->选项->符号表中设置符号表路径
.h/.hpp头文件,由于预编译时被“#include”指令包含
.obj通用对象文件格式文件,编译与链接的中间二进制文件

2.2 参数

注意,cl的参数一般使用“/”开头,但不排斥“-”,另外,很多参数可以与值无空格使用,如“\DWWW=10”与“\D WWW=10”都是预定义了WWW这个宏。

参数名描述
/Tc指定C源文件,如/Tp test.c,不使用会根据尾缀判断
/Tp指定C++源文件,如/Tp test.cpp
/EH异常处理模式,一般用/EHsc
/Fe指定输出exe文件名
/I头文件目录,适用于不在当前目录和INCLUDE变量目录下的头文件
/c只编译,不链接
/Z7不产生.pdb/.lib等文件,将所有调试信息存入.obj文件中
/ZI产生.pdb/.lib等文件,用于调试等需求
/C编译期间保留注释
/sdl进行安全检查,这对老式代码不兼容
/std指定c++版本,可选:
/std:c++14
/std:c++17
/std:c++latest
/D预定义宏,如/D MAR=10,那么相当于源码中定义了一个宏MAR,值为10。若没有值,默认为0。下例中,若cl /TP test.cpp /EHsc编译,则运行./test输出NO WWW,若cl /TP test.cpp /EHsc /D W=10编译,输出为宏值10
/RTC进行运行时错误检查,可选:
/RTCs
/RTC1
/RTCc
/RTCu
/utf-8指定为utf-8编码
/LD、/LDd创建动态库dll,会产生test.DLL和test.obj。配合/ZI会产生对应的test.lib和test.pdb,示例:
cl /TP test.cpp /EHsc /sdl /std:c++14 /LDd
/link用于指定链接器(link.exe,由cl负责调用)链接的静态链接库(.lib)。对于动态库DLL,无法直接如gcc那样通过参数链接,需要指定对应的静态链接库(只起到指导动态库名称作用)。有以下三种情况:
源码开始:cl /TP call.cpp /link test.lib
中间码开始:cl call.obj /link test.lib
源码中加入编译指令:#pragma comment(lib, "test.lib")
/?cl的帮助指令,可以cl /? > file.txt将帮助信息重定向而方便阅读
/*/D预定义宏的示例
cl /Tp test.cpp /EHsc /D WWW=10 ,输出10
cl /Tp test.cpp /EHsc /D WWW, 输出0
cl /Tp test.cpp /EHsc,输出No WWW
*/
#ifndef WWW
#define WWW "NO WWW"
#endif
#include <iostream>
int main(int argc, char const *argv[]) {
	std::cout << WWW << std::endl;
	return 0;
}

2.3 _declspec(dllimport)和_declspec(dllexport)

对于MSVC的编译器cl,想要使用DLL库,必须指定DLL中哪些成员是导出的(dllexport),哪些成员是从DLL库导入来使用的(dllexport),类似于模块化import/export。同时,由于DLL和EXE的内存分配方式是独立,因此需要注意采用相同的运行时库来统一内存分配,以便在传输C++对象时能够正确释放内存。下例中统一使用了/MD链接了MSVCRT.LIB。cl提供了以下四种指令对应四种运行时库(前三种试验验证可以正确传输object,第四种/MTd失败)。由于原因不在编译而在于运行,故单独编译DLL和EXE都没有任何ERROR提示

ParamLibrary
/MDMSVCRT.LIB
/MDdMSVCRTD.LIB debug lib
/MTLIBCMT.LIB
/MTdLIBCMTD.LIB debug lib

由于默认编译时DLL和EXE对应的VC运行库不一样,故在C++中推荐使用C数组代替C++ string对象进行DLL与EXE对接。

//DLL库源码,cl /TP library.cpp /EHsc /sdl /std:c++14 /LDd /ZI /MD
#include <iostream>
#include <string>
#ifndef DLL_EXPORT
#define DLL_EXPORT __declspec(dllexport)
DLL_EXPORT void print(std::string args){
	std::cout << args << std:: endl;
}
#endif

//DLL库使用头文件
#pragma once
#pragma comment(lib, "library.lib")
#include <string>
#define DLL_IMPORT __declspec(dllimport)
DLL_IMPORT void print(std::string);

//外部程序调用, cl /TP call.cpp /EHsc /sdl /std:c++14 /MD
#include "test.hpp"
int main(int argc, char const *argv[]){
	print("Test string");
	return 0;
}

但对于gcc/g++,没有这个限制,因为会默认会导出DLL中所有全局的成员,只需要在编译时声明就好(或include对应头文件)。也没有运行时内存分配问题,估计是编译器做了统一。统一使用-l参数指定链接的动态/静态库,-L参数指定库路径。

三、共享库生成与链接

共享库生成图示

3.1 静态共享库生成

对于dev-c/c++的gcc/g++,在linux/win下,借用ar命令,$(FileNameNoExt)代指无后缀文件名, 下同,注意静态库需要先将源码编译成.o文件,ar命令只负责打包

ar cr lib$(FileNameNoExt).a $(FileNameNoExt).o

对于MSVC的cl,借用lib命令,这只是windows下开发,将cl编译产生的obj文件处理成lib库

lib /OUT:test.lib test.obj

3.2 动态共享库生成

(1)linux下的.so共享库

g++ -shared -fPIC -o lib$(FileNameNoExt).so $(FileNameNoExt).o

也可以window下生成后先改名为dll文件通过链接生成exe.再改回so,因为g++寻找链接库时自动加后缀根据系统,但链接时库类型会根据内容判断。即会去找.dll尾缀文件当库,但找到后链接时更加库内容判断库实际是.so,运行exe时会寻找so尾缀库文件

(2)window下的.dll共享库

g++ -shared -Wall -o lib$(FileNameNoExt).dll $(FileNameNoExt).o

共享库源文件中若存在main函数,main函数会被忽略而不被编译为共享库。

对于MSVC的DLL,如前使用cl命令的/LD指令生成。

3.3 共享库的链接

g++ -o $(FileNameNoExt).exe -L <SelfLibarayPath> -l <LibNameWithoutExtAndPrefix> $(FileNameNoExt)(.o/.cpp/.c)

若为.o文件,只需链接不需编译。注意:-l后库名不包含规范前缀lib和尾缀.so/.a/.dll, 会更据系统选择动态库尾缀.dll /.so,g++会自动加上,如-ltools => libtools.so/libtools.a, 两个都存在默认选静态库,想选静态库可以直接多文件链接方法,如多个.o一样, -L参数不指定库文件路径,则会去标准库路径找

静态库链接原理示意如下,链接器会将静态库中所需目标文件与主文件链接在一起生成可执行文件。

link

四、Editplus命令配置

注意用的共享库是自定义的libtools.dll/libtools.so/libtools.a
编译+链接+执行:g++ $(FileName) -I "$(FileDir)" -L "$(FileDir)" -ltools -o $(FileNameNoExt).exe && $(FileNameNoExt)
编译:g++ -c $(FileName) -I "$(FileDir)" -o $(FileNameNoExt).o
链接:g++ $(FileNameNoExt).o -L "$(FileDir)" -o $(FileNameNoExt).exe -ltools
执行:$(FileNameNoExt)
DLL动态库编译:g++ -shared -Wall -o lib$(FileNameNoExt).dll $(FileNamebr>
SO动态库编译:g++ -shared -fPIC -o lib$(FileNameNoExt).so $(FileName)

五、C++对象创建的两种方式

在C++里面可以new对象,也可以直接声明对象。

5.1 C++编译器内存区域划分

1.静态存储区域:主要保存全局变量和静态变量。 生存期:整个程序。static关键字

2.:存储动态生成的变量。生存期:自己来决定。需要手动释放内存。

3.:存储调用函数相关的变量和地址等。生存期:所处的语句块(既{}的范围)

对于一个自定义类Person, 有两种创建对象的方式

5.2 声明对象与对象数组

Person person(name, id);
Tricycle car[2]; //无参数构造器
Tricycle car[2] {{"A0001"}, {"A0002"}};//有参数构造器
Tricycle car[] = {Tricycle("A00011"), Tricycle("A00012")}; //这种数组形式也可

在声明的时候,C++编译器在中给它分配了一个空间存放所有的成员变量,但是为了节约内存空间,成员函数 被存放在了一个公共区域,这个类的所有的对象都可以共同享有。调用这个对象的成员变量和成员函数时用“.”操作符。对于对象数组,分为无参数构造和有参数构造

5.3 new关键字创建对象

(1)创建单个对象

Person * person = new Person(name, id)

若采用Person person = * new Person()的形式,实际上左右两边对象指针并不相同,左侧通过声明分配了栈内存空间,右侧通过new关键字分配了堆内存空间,这样的创建会导致堆内存无法释放而产生内存泄漏。若需要直接获取对象,可以改成对象引用的形式, 以“&”操作符声明一个对象的引用person1,这种形式更类似于java的对象创建。引用的指针与对象的指针指向相同内存地址。

Person person0("Zhang Hongning", "332527197608218532");
Person & person1 = person0; //关于person0的引用

//这种形式更像java创建对象,左边只是声明引用而没有分配空间
Person & person2 = *new Person("Zhang Hongning","332527197608218532");

对象引用使用时必须确保引用的对象在作用域内。如作用域在函数内的对象不能将引用返回到函数外。否则会报错或产生不可预料结果。

string & getPerson () {
	string name = "Zhang hongning";
	return name;
}
int main() {
	string & str = getPerson();//会有warning或bug,引用的对象已经销毁
}

下面方法正常编译, 因为是new创建的堆内存对象。

string & getPerson () {
	string * name = new string("Zhang hongning");
	return * name;
}
int main() {
	string & str = getPerson();//没有warnings
}

(2)通过无参数构造器创建对象数组

Person * persons = new Person[2]

对象数组的数据结构示意

array

(3)通过有参数构造器创建对象数组

Person * persons2 = new Person[2] {
		{"zhang hongning", "332526"},
		{"zhang san", "332527"}
};

通过new创建的实例返回的是对象指针, 并在中分配了空间,需要程序员显式释放空间,与delete或delete [] 操作对应。否则会导致内存泄漏。此时的对象数组无法用sizeof获取数组大小,sizeof(person2) == sizeof(person2[0])。

不过,可以通过创建对象指针数组的形式,从而可以获得数组大小。

Tricycle * car[] = {
	new Tricycle("A0003"),
	new Tricycle("A0004"),
	new Tricycle("A0005")
};
std::cout << (sizeof(car)/sizeof(Tricycle *)) << '\n';//数组大小为3,每个元素是对象指针

(4)销毁对象

delete person

detete对象时会调用对象的析构函数销毁对象。对象指针直接调用对象成员变量可以使用指向操作符“->”

delete对象时会自动调用对象的析构函数,但值得注意的是,储存在栈中的成员变量在对象被销毁时会被编译器自动销毁,包括通过声明构建的成员对象和基本类型。

但通过new关键字创建的成员对象(即保存在堆内存中的对象)需要在析构函数中显式地释放内存空间,否则会在程序结束时也无法释放而导致内存泄漏。故需注意对象中是否有储存在堆中的属性。

delete关键字用于对象数组,只会数组所占内存空间(上图矩形部分)并销毁首个元素对象并调用去析构函数。而上图椭圆部分剩余几个对象并未被释放且程序结束也不会释放而导致内存泄漏。

但如果是基本类型数组,由于其数据结构不存在椭圆部分,仅存在一个连续的数组内存空间保存基本类型值,故当delete操作能够完全释放基本类型数组。

对成员变量定义在栈中的对象,以Tricycle为例

Person owner("h", "zz");//在栈内存创建实参Person
cout << "实参Person owner\t" << &owner << endl;
Tricycle * car = new Tricycle("A0001", owner);
delete car;

结果输出如下

实参Person owner	0x61fce0
create Person whose point is 0x2562500 //car对象成员变量创建
构造函数形参Person owner 0x61fdb0
create Tricycle whose point is 0x25624e0 //car对象创建
delete Person whose point is 0x61fdb0 //构造器栈中形参用完自动销毁
delete Tricycle whose point is 0x25624e0 //delete销毁car对象本身
delete Person whose point is 0x2562500 //car对象成员变量销毁
delete Person whose point is 0x61fce0 //main介绍自动销毁栈中实参

由上看出形参存储在栈内存中,构造器用完自动销毁形参,而实参是在外面主函数范围内,最后才自动销毁。

创建car对象时,先在栈内存中创建成员变量owner,再创建car对象,上例中对象储存中堆内存中。故需要delete销毁

delete销毁car时,先销毁car对象自身,car对象结束生命周期后,其栈内存中的成员变量owner被自动销毁。

(5)销毁对象数组

delete [] persons

销毁对象数组及数组中的元素,会逐一调用每个对象的析构函数。

六、指针

1. 指针的本质

指针本质上是C++编译器储存各类数据类型内存地址和数据类型的一种特殊变量,是一种特殊的整数类型,保存在中。指针所指内存区域大小由编译器判断。理解指针本质就能很好理解为何numpy的数据统一保存在一个char类型指针所指数组中

2. 内存泄露、内存释放与指针悬摆

内存泄漏(Memory Leak)是指程序中已动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。因此在使用完毕后应当使用delete操作符手动释放占据的堆内存。delete操作符的参数是指向该内存地址的指针,删除完毕后的指针仍然指向该地址,是为悬摆指针,对悬摆指针进行操作会造成错误,应当将delete后的指针赋值为空指针(nullptr/NULL/0),下面定义data在释放内存后再次调用指针会产生不可预料的错误。因为编译器会认为这里是没有东西而另行他用,通过悬摆指针修改内存会导致他用产生问题。

int * data = new int;
*data = 5;
printf("%d\t%p\n", *data, data);
delete data;
printf("%d\t%p\n", *data, data); //值为莫名其妙的值而非5

3. 常量指针与常量成员函数

常量由const关键字定义,注意由宏命令#define的常量实际上在进入编译器时已经替换成为字面量,故它不存在指针操作。常量无法用指针修改值

const int * pcdata = new int(5);
printf("%d\t%p\n", *pcdata, pcdata);
*pcdata = 6; //报错,无法修改常量
#define DATA 7
printf("%d\t%p\n", DATA, &DATA); //报错,不能进行取指针操作

对象如果是常量声明,那么只能调用常量函数,常量对象与常量函数均由const修饰

const Person * owner = new Person("Zhang hongning", "3538");
cout << owner->getName() << endl;//getName声明为常量函数
owner -> setName("zju");//报错,修改常量对象

在实践中,经常将函数形参声明为常量指针,这样得到的形参对象是常量对象,从而将对象本身传递给函数的同时避免函数对对象的修改。

Person person("Zhang", "ID");
void getName(const Person * person) {
	person -> getName();//正常
	person -> setName("ZHN");//编译器报错
}
int main () {
	getName(&person);
}

由于形参声明是常量,故无论实参是否为常量均被视为常量。但若形参为变量,那么实参必须为变量才能通过编译。

七、对象重载构造器共享代码

1.placement new策略

通过new(this)覆盖原先,注意new Tricycle会创建新的对象而非在原先内存上覆盖

new (this) Tricycle(license, new Person("admin", "00001"));

2.通过函数默认值

类函数默认值一般在声明中描述,声明与定义不能重复默认值。

Tricycle(string, Person * owner = new Person("admin", "00001"))

3.成员函数共享

通过定义一个private的成员函数实现构造器通用部分,如init函数。

class Tricycle {
	private:
		void init() {
			//初始化代码块
		};
	public:
		Tricycle(){init();}
		Tricycle(string) {init();}
};

4.委托构造函数

自C++ 11开始,可以在构造函数实现中委托另外一个构造函数完成部分任务。

构造函数由初始化部分函数体组成,其中初始化部分在函数体前形参声明后面,以“:”开始。在这个地方可以用成员变量名(初始化参数)的形式初始化,也可以调用其他成员函数进行构造。在java中,构造器使用大大简化,所有初始化均在函数体内完成,通过this()调用其他重载构造器,通过super()调用父构造器。

Tricycle::Tricycle(string license):Tricycle(license, new Person("admin", "00001")) {
	//构造器其他任务
}

Tricycle::Tricycle ():license("A00001"), owner(new Person("admin", "00001")) {
	//构造器函数体
}

对于常量和引用,由于无法进行赋值修改,必须在初始化区域初始化。对于变量,可以再函数体中用“=”进行调用复制构造函数初始化。

class Tricycle {
	private:
		const string license;
		const Person * owner;
};

//该形式编译器报错
Tricycle::Tricycle (string license, Person * owner) {
	this -> license = license;
	this -> owner = owner;
}

//该形式通过编译
Tricycle::Tricycle(string license, Person * owner) :license(license), owner(owner) {
	//函数体
}

八、高级函数

1. 复制构造函数

Person person0();
Person person1 = person0;

对于上述操作,等号的赋值会将person0的成员变量全部拷贝给person1,这个过程是由Person类的复制构造函数完成的。复制构造函数与构造函数属于重载函数的关系,其形参为一个待复制对象的引用,一般声明为常量引用。

Person(const Person & person);

和构造函数一样,若用户为定义,编译器会自动提供一个默认复制构造函数,默认构造函数对成员变量是浅复制。默认构造函数形式推理如下:

Tricycle (const Tricycle & car) {
	license = car.license;//对象变量,调用复制构造函数
	owner = car.owner;//指针变量,共用指针
}

故对于指针或者引用类型的成员变量,会共享,一旦delete会导致另外一个对象访问该指针或引用所指地址出现错误。

Tricycle & car0 = *new Tricycle("A0001");
Tricycle car1 = car0;
std::cout << car0.getOwner() << '\t' << car1.getOwner() << '\n';
delete &car0;
Person * tmp = car1.getOwner();
std::cout << *tmp << '\n';//此时解引用会报错,指针所指内存地址已经释放

为了避免浅复制所导致的问题,一般会对有指针/引用成员变量的类自定义复制构造函数,例如:

Tricycle (const Tricycle & car) {
	license = car.license;//对象变量,调用复制构造函数
	owner = new Person;//重新分配内存地址
	*owner = *(car.owner);//调用Person复制构造函数
}

2.移动构造函数

8.2.1 左值与右值

左值指的是具名的、可获取指针的、生命周期较为长久的数据类型,右值指的是不具名的、无法获取指针的、临时数据类型。由于左值一般居于等号左侧,右值居于等号右侧,因此得名。

int a = 1;//a为左值,字面量1为右值;
int getDouble(int a) {
	return 2 *a;
}
int b = getDouble(a);//a,b为左值,getDouble(a)返回值为右值

8.2.2 左值引用与右值引用

左值引用即我们常见的对具名变量的引用,通过单修饰符“&”声明。为了对右值进行更加有效利用,改变右值变量的临时性,在C++ 11中引入了右值引用,用双修饰符“&&”声明。右值引用能够有效减少不必要的临时内存开销。注意,右值引用本身是一个左值,故而可以持久存在。

int v = 1;
int & vl = v;//左值引用 
int & a = 1;//编译报错,右值无法创建直接左值引用
int && vr = 1;//右值引用
int && b = v;//编译报错,左值无法创建右值引用
vl= vr;//将右值引用的值拷贝给左值引用
const int & vc = 1;//常量左值引用可以用于常量/变量的左右值,算是奇葩,但是是常量而无法修改

8.2.3 移动构造函数

拷贝构造函数是将右值或左值创建一个常量左值引用,并分配新内存给新建对象,复制参数。如使用右值作为初始化参数,右值只是临时使用了一块内存,有点浪费。在C++ 11中引入了移动构造函数,参数类型是一个右值引用,编译器会将新对象的指针指向当前右值参数,故而无需重新分配对象内存空间。

Tricycle (Tricycle && car):license(car.license),owner(car.owner) {
	car.owner=nullptr;//必须将右值的指针成员变量指向空,不然右值析构会销毁它
}

在调用时,若有声明移动构造函数,右值参数优先传给移动构造函数。若没有,由于拷贝构造函数参数类型是常量左值引用,可以接受右值参数而进行拷贝构造。

3.编译阶段常量表达式

编译阶段,C++编译器会将代码进行尽量的优化,例如会将#define定义的宏常量全部替换为字面量

#define DATA 1
printf("%d", DATA);   =>   printf("%d", 1);

也会把const常量进行必要的整合,如下面会将2006与10计算后存储。而非保存两个量。

const int decade = 10;
int year = 2006 + decade;   => int year = 2026;

类似的,针对2006 + getValue()这样含有函数表达式的例子,C++引入了常量表达式,这是一种特殊的函数,用constexpr修饰,且必须有返回值,返回值只能保含字面量或其他常量表达式。

constexpr int getCentury() {
	return 22;
}

constexpr double getPi() {
	return (double) getCentury() /7;
}

九、运算符与运算符重载

C++中的运算符分为一元、二元与三元运算符。一元运算符有!、++、–等,二元运算符有=、==、<=、>=、!=、&&、||、+、-、*、/、%等,三元运算符有?:等。其运算符应用规则与java基本一致。
只得一提的是,几乎所有C++运算符都支持运算符重载,即与特定的类函数关联。所有的运算符都有对应的函数返回值,等号“=”也不例外。但等号运算是右结合律,这与其他运算符不同。

int m = 6;
int v = m=7;
cout << m << v;//m和v都变成7,赋值运算右结合律

在C++中,比较特殊的是逗号“,”也可以是一种运算符

int m = 6;
int v = (7, m);//v变成6,逗号运算会舍弃左侧值,但注意没有括号就变成了声明v,m两个变量,会报错,虽然正常也不会这样用。

重载运算符+、-、*、/等可以简化类与类的计算代码,其重载基于类成员函数,形式如下,其中"SYMBOL"需要替换至具体运算符。

returnType operatorSYMBOL(parameter list);

下面以Counter类为例

class Counter {
  private:
    int data;

  public:
    Counter(int);
    ~Counter() {}
    int getData() const;
    const Counter& operator++(); //prefix ++运算符
    const Counter operator++(int);//postfix ++运算符
		const Counter operator=(const Counter &);//Counter a; a=b
		operator int();//对象转int
};

1. 自增运算符++

//模拟 ++a,先+1再赋值
const Counter & Counter::operator++() {
  data ++;
  return *this;
}

//模拟a++, 先赋值再+1,为了区别++a,有形参int
const Counter Counter::operator++(int) {
  Counter tmp(*this); //调用复制构造函数
  data++;
  return tmp;
}

2. 加法运算符+

对于二元运算c1 + c2,C++编译器将其看成c1.operator+(c2)

const Counter Counter::operator+(const Counter & beta) {
	return Counter(data + beta.getData());
}

3. 赋值运算符=

对于声明变量时赋值运算,调用的是复制构造函数来构建对象。但对于已经声明的变量,赋值运算调用重载赋值运算函数operator=();其操作与复制构造函数相似,但不用重新创建对象。该方法与构造函数、析构函数、复制构造函数。

const Counter Counter::operator=(const Counter & beta) {
  if (this == &beta) {
    return *this;
  }
  //如果是指针成员,注意深浅复制区别
  data = beta.getData();
  return *this;
}

与复制构造函数、移动构造函数对应,有复制赋值运算(如上)和移动赋值运算(如下),区别也是在于左右值引用。

const Counter Counter::operator=(Counter && beta) {
  if (this == &beta) {
    return *this;
  }
  //如果是指针成员,注意深浅复制区别
  data = beta.data;
  beta.data = nullptr;//原因与移动构造函数一样
  return *this;
}

故同样需要同复制构造函数那样考虑深浅复制问题,且如果自我赋值需要防止空对象。

4. 转换运算

(1)内置类型转对象
这个过程的实现本质上与复制构造函数一样,复制构造函数的参数类型为当前类的实例的引用,而这里将参数类型改为内置类型。这里函数即是普通的重载构造器,又是转换运算。

Counter::Counter(int val):data(val) {}
Counter c = 16;

(2)对象转内置类型
这个过程通过一个无返回值声明的operator成员函数实现,形式为operator type(),type可替换为其他内置类型

Counter::operator int() {
	return data;
}
ios_base::operator bool(){
	//这是IO流判断到达尾部的方法,如while(cin){}, 形式上类似Perl语言的while(<STDIN>)
}

值得一提的是,在Java中并不支持开发者重载运算符。java唯一自己重载的运算符是“+”,用于字符串拼接。

//java源码
String s = "abc" + "bcd";
//编译后转换, java开发更推荐使用StringBuilder
String s = (new StringBuilder("abc")).apend("bcd").toString();

十、类的继承与多态

1. 继承

C++类继承与java类似,只是形式不同。 但C++支持多继承,被继承类之间用逗号隔开

class Children: <权限修饰符> Father1,<权限修饰符> Father2, ... {};//C++
<权限修饰符> class Chidren extands Father implements <接口1>, <接口2> {}//Java, 权限修饰与C++意义不同
class Chidren(Father): //Python

2. 权限修饰符

与Java类似,C++有public、protected和private三种权限修饰符。public/protected修饰的成员变量或方法能够被子类继承访问,private修饰的只能在类内访问。与Java不同的是,C++中没有包的概念,只有public类型的成员变量或成员方法能被外部访问,而java中protected修饰能在相同包内访问。此外C++中虽然没有类本身的权限修饰,像java那样public class,但是对类的继承有权限修饰。

权限修饰符自身成员权限控制父类成员权限控制
public可以外部访问、继承公有继承
protected无法外部访问,可以被继承保护继承
private无法外部访问,无法被继承私有继承
class Chinese:public Person {}; //公有继承:继承Person类中public/protected的成员,并设为自身的public成员
class Chinese:protected Person {}; //保护继承:继承Person类中public/protected的成员,并设为自身的protected成员
class Chinese:private Person {}; //私有继承:继承Person类中public/protected的成员,并设为自身的private成员

3. 继承类的构造与销毁

继承类的构造与销毁其实是按照堆栈的出入顺序分别调用各级继承类的构造函数和析构函数完成构造与销毁。如下所示。创建时先调用父类构造器,再调用子类构造器,销毁时调用析构的顺序则相反,类似于先近后出的堆栈逻辑。当多继承时,在类声明中位置较前的父类先构造,后销毁。

class Chinese:public Person {
  private:
    string name_CN;
    string nation;
  public:
    Chinese(string, string, string, string);
    Chinese(string, string);
    ~Chinese();
    string getChineseName() const;
    string getNation() const;
};

int main() {
	Chinese * chinese = new Chinese("ZHN", "310058", "张洪宁", "Han");
	delete chinese;
}

输出:
create Person whose point is 0xff64c0
create Chinese Person whose point is 0xff64c0
delete Chinese Person whose point is 0xff64c0
delete Person whose point is 0xff64c0

继承类的构造参数传递
若构建继承类时有父类的参数传入,可以在构造函数的初始化区调用父类构造函数初始化。

Chinese::Chinese(string name, string id, string name_CN, string nation):Person(name, id),name_CN(name_CN),nation(nation) {//调用了Person构造函数
  cout << "create Chinese Person whose point is " << this << endl;
}

4. 函数覆盖(重写)与多态

函数覆盖

java和C++都支持在子类中覆盖父类的函数,重新定义一个符合子类要求的函数。函数覆盖要求与父类的函数返回类型及函数签名(函数名+形参类型+形参顺序)相同

值得注意的是,若父类函数有多个重载函数,覆盖一个,其他几个也无法使用,以免造成不必要的错误。

但是,若还想调用父类的成员函数(包括被覆盖的),需要用全限定名调用

Chinese.Person::getName(); //类似java和python的super
rect->Shape::draw();//指针形式

虚成员函数与多态
在java中可以采用父类声明而子类初始化的形式实现多态性,在C++中也是类似。

Person * chinese = new Chinese;//C++多态
Person chinese = new Chinese();//Java多态

但C++与java不同之处在于,java在多态声明时,若调用重写函数,会自动调用子类的函数。而C++默认调用父类函数。因此,C++中为了支持多态,引入了虚成员函数。在父类将待重写函数声明为虚成员函数后即会自动调用子类重写函数。

Person * chinese = new Chinese("ZHN", "310058", "张洪宁", "Han");
cout << chinese -> getName() << endl;//输出的是父类结果ZHN

虚成员函数用virtual修饰。其背后原理,很多编译器实际上创建虚成员函数表,里面保存了虚成员函数的指针。如前,在创建子类对象时,会先调用父类构造器,此时虚成员函数指针指向父类函数,当调用子类构造器,又会指向子类,从而实现了多态。

class Person {
	public:
		Person (string, string);
		virtual ~Person ();
		virtual string getName() const;
		....
};

Person * chinese = new Chinese("ZHN", "310058", "张洪宁", "Han");
cout << chinese -> getName() << endl;//输出的是子类结果张洪宁

注意: 同java的多态一样,此时无法直接调用子类独有的方法,在java中必须进行强制转换。在C++中也是类似。

//java实现强制转换,如不行会抛出ClassCastException
Person chinese = new Chinese("ZHN", "310058", "张洪宁", "Han");
System.out.println(((Chinese) chinese).getNation());

//C++实现强制转换,但类型不兼容不会产生提示,指针任然指向原先地址,故一般不直接采用强制转换,除非你清楚
Person *chinese = new Chinese("ZHN", "310058", "张洪宁", "Han");
cout << "Person\t" << ((Chinese *)chinese) << endl;

//C++用dynamic_cast <目标指针指针类型> (待转换指针)的形式,如果不可转会返回空指针,程序猿可以用条件语句进行判断
Person *chinese = new Chinese("ZHN", "310058", "张洪宁", "Han");
cout << "Person\t" << (dynamic_cast <Chinese *> (chinese))->getNation() << endl;//正常
Person * person = new Person();
cout << "Person\t" << ((dynamic_cast <Chinese *> (person)) == nullptr) << endl;//空指针

同时由于多态本质上是创建了子类对象,所以一般会将析构函数也声明为虚成员函数,从而销毁释放子类对象(此时会先调用子类析构,再调用父类析构,同前)。

切除

多态的本质是创建了指向子类对象的指针,并因为指针类型声明为父类而屏蔽了内存地址中子类独有部分,故多态的实现必须利用指针和引用,正如前面提到,java的声明初始化其实是一种类似C++引用的声明。下面这种按值传递,实际上是将子类对象复制给父类对象,调用了父类的复制构造函数,父类中不包含的部分自然而然就会被切除

Person chinese = Chinese("ZHN", "310058", "张洪宁", "Han");
cout << "Person\t" << chinese.getName() << endl;//输出父类参数“ZHN”, 子类信息缺失

Person &chinese = *new Chinese("ZHN", "310058", "张洪宁", "Han");
cout << "Person\t" << chinese.getName() << endl;//输出子类参数“张洪宁”

5. 抽象类

在java中有抽象类与抽象方法的概念,它们通过abstract关键字来定义。抽象类无法直接创建自身对象,而必须由具体的实现子类来创建多态对象。在C++中也是类似。前面提到C++利用virtual关键字定义虚函数来实现类的多态,但这个父类和虚成员函数仍然可以直接创建并调用,C++再此基础上通过将虚成员函数初始化为0来表明当前函数是抽象的,必须在子类重写,从而实现了抽象类与抽象方法定义。初始化为0的虚函数称为纯虚函数。同样,抽象类可以局部实例化而产生一个子抽象类。

//java
public abstract class Person {
	public abstract String getName();
}

//C++
class Person {//抽象类
	public:
		virtual string getName() = 0;//纯虚函数,因为函数本身也是储存在指针所指地址的数据类型,故可以声明初始化。因为内容为0,必须覆盖
};

class Chinese: public Person {
	public:
		string getName();
}

string Chinese::getName() {
	return "张洪宁";
}

值得一提的是,C++中抽象方法(纯虚函数)可以提供实现的函数体,但在java中抽象方法只能被覆盖重写,不能提供实现的函数体。

//java, 会报错不通过编译
public abstract String getName() {
	return "Zhang Hongning";
}

//C++, OK
virtual string getName() = 0;

string Person::getName() {
	return "zhn";
}

十一、C++与系统的交互

1. 命令行参数的获取

C++同java一样,在主方法处的形参可以获取命令行信息,操作由编译器与系统完成。(https://www.cnblogs.com/Allen-rg/p/6762437.html)

/*
* @param argc 整型,系统命令行参数个数
* @param argv[] 字符串指针数组,代表所有命令行参数,其中第一个是当前执行程序名
*/
int main(int argc, char const *argv[]){

}

//java的主方法,由于java字符串数组自带长度属性,没有C++的第一个参数,此外java没有第一个值代表程序名
public void static main(String[] args) {

}

2. 程序执行完毕的暂停

如下函数可实现系统那样的“按任意键继续…”

system("pause");//即bat脚本中的pause,若为pause>nul,没有“任意键继续...”

十二、C++的IO操作

1. C++的IO方法类库体系

IO

2.文件流fstream

数据流的概念与java中一样,文件流fstream中包括文件输出流ofstream、文件输入流ifstream和文件流fstream对象进行IO操作

#include <fstream>
#include <iostream>
#include <string>
int main(int argc, char const *argv[]) {
	fstream f(filename, ios::in);
	if (f.is_open()) {
		string greet;
		while (getline(f, greet)) {
			cout << greet << '\n';
		}
		f.close();
	} else {
		std::cerr << "File Not Exists" << '\n';
	}
}

十三、函数指针与Lambda表达式

函数也是一种数据类型,也有它的内存地址,因此函数也有它的指针。实际上,咱定义的函数的名称就可以理解为一个函数指针。但函数指针的创建与普通函数创建还是不一样的

void (*functionPtr)(int);//声明函数指针,需要圆括号
void * functionPtr(int);//声明返回值为任意指针的函数

functionPtr(10);//函数或函数指针的实现函数调用

Lambda表达式是一种简单的函数创建形式,在C++中,其实际上是创建了一个函数并返回其函数指针。与否规则与java有类似之处。

//C++ Lambda,实际上创建的是函数的栈指针,可用auto进行类型推断,也可显式声明函数指针
auto functionptr0 = [local_val](int x){return x + 1;};//[]声明使用的外部局部变量,()显式声明函数参数,{}为函数体
int (*functionptr1)() = []()->int{return 1;}//可以用“->”指定返回类型,这与java对该符号的使用不同.
functionptr0(2);

值得注意的是,[]中外部局部变量的使用可以有多种策略,实现按引用传递、按值传递、全部使用、指定使用。

参考文献

[1] Rogers C, Jesse L.译者:周进、裴强.C++入门经典(第六版)[M].人民邮电出版社.北京:2016.

[2] 简书-我理解的右值引用、移动语义和完美转发

[3] CSDN-cl命令参数详细介绍(转)

[4] CSDN-gcc/g++ 链接库的编译与链接

[5] Stanley B.L, Josee L, Barbara.E.M.译者:王刚,杨巨峰.C++ Primer中文版(第5版)[M].电子工业出版社.北京:2013.

[6] CSDN-关于Dll中导出string的问题

[7] IT屋-C ++传递std :: string通过引用dll中的函数

[8] 博客园-C++文件读写详解

[9] 博客园-C++之Lambda表达式

[8] 其他零散而不记得来源的参考资料

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Code::Blocks 是一个开源的集成开发环境(IDE),主要用于编写和调试C、C++和Fortran程序。如果 Code::Blocks 无法编译C程序,可能存在以下几个可能的原因: 1. 缺少C编译器:Code::Blocks 需要与C编译器一起使用才能编译C程序。确保已经安装了适当的C编译器,例如MinGW(Windows平台上的常见选择)或GCC(Linux平台上的默认编译器)。可以通过在Code::Blocks的设置中配置编译器路径来解决此问题。 2. 配置问题:在Code::Blocks中,需要正确配置编译器才能进行编译。检查Code::Blocks的编译器设置,确保已选择正确的编译器,并且路径设置正确。 3. 代码错误:如果代码中存在语法错误或逻辑错误,编译器将无法将其编译成可执行程序。在编译之前,请确保代码正确且没有错误。 4. 编译器版本不兼容:Code::Blocks可能不兼容特定版本的编译器。尝试使用不同的编译器版本或更新Code::Blocks版本,以解决此类问题。 5. 系统环境问题:某些系统环境可能会导致Code::Blocks无法编译C程序,例如缺少必要的库文件或配置错误。检查系统环境是否满足编译要求,并确保没有其他程序或设置干扰编译过程。 总之,如果Code::Blocks无法编译C程序,需要检查编译器的安装和配置,并确保代码正确且没有错误。如果问题仍然存在,可能需要考虑更换编译器或更新Code::Blocks版本。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值