万丈高楼平地起——C++入门(上卷)

x33g5p2x  于2022-05-06 转载在 其他  
字(11.3k)|赞(0)|评价(0)|浏览(146)

😏前言

哈喽,大家好,我是鹿九丸,今天就要开启C新的篇章了,不知道大家已经准备好了没有,数据结构系列还有一些小的部分没有收尾,不过大部分内容已经完成了,后续我会把数据结构初阶做一个相关的总结,方便大家查阅观看,C这个编程语言的重要性相信大家都清楚,在这里不做过多阐述,但请大家相信丸丸,丸丸会总结出尽量全面和详细的知识,内容方面大家完全不需要担心,丸丸一定会保质保量地,好了,多余的内容不再过多阐述,C++方面的细节很多,我会尽可能地把各种细节进行深挖讨论,来帮助大家尽可能打下良好地基础,废话不多说,直接开始!

如果大家在看我的博客的过程中或者学习的过程中以及在学习方向上有什么问题或者想跟我交流的话可以加我的企鹅号:2361038962 \color{red}{2361038962}2361038962,或者寄邮件到相应的邮箱里:2361038962 @ q q . c o m \color{red}{2361038962@qq.com}2361038962@qq.com,我会尽量帮大家进行解答!

🧡1.C++关键字

C++共计63个关键字,包括C语言的关键字32个:

asmdoifreturntrycontinue
autodoubleinlineshorttypedeffor
booldynamic_castintsignedtypeidpublic
breakelselongsizeoftypenamethrow
caseenummustablestaticunionwchar_t
catchexplicitnamespacestatic_castunsigneddefault
charexportnewstructusingfriend
classexternoperatorswitchvirtualregister
constfalseprivatetemplatevoidtrue
const_castfloatprotectedthisvolatilewhile
deletegotoreinterpret

🧡2.第一个C++程序

#include<iostream>
using namespace std;
int main()
{
	cout << "hello C++" << endl;
	return 0;
}

🧡3.命名空间(namespace)

在C/C++中,变量、函数和后面要学到的类都是大量存在的,这些变量、函数和类的名称将都存在于全局作用域中,可能会导致很多冲突。使用命名空间的目的是对标识符的名称进行本地化,以避免命名冲突或名字污染,namespace关键字的出现就是针对这种问题的。

命名冲突举例:

在库函数<math.h>中已经存在了sqrt函数的定义,此时我们又定义了一个全局变量sqrt此时就会出现sqrt重定义的现象,这就是命名冲突。当然,命名冲突不止包含这一种,还包括全局变量与全局变量的命名冲突,局部变量与局部变量的命名冲突,全局函数与全局函数的命名冲突,总结来说,具有相同作用域的变量名或者函数名如果相同就会发生命名冲突。

💖3.1 命名空间定义

定义命名空间,需要使用到namespace关键字,后面跟命名空间的名字,然后接一对{}即可,{}中即为命名空间的成员。

💘3.1.1 普通的命名空间

💖命名空间的使用及举例
namespace Test//Test为命名空间的名称
{
    //命名空间中的内容,既可以定义变量,也可以定义函数,也可以定义类型
	int sqrt = 0;//自己的命名空间,可以和库函数的函数名相冲突
	void fun()
	{
		return;
	}
	struct stu
	{
		int b;
		char c;
	};
}

下面是命名空间的使用(默认已经包含了相应的头文件)

使用格式:

命名空间名称::命名空间中的变量名或者函数名//::是域作用限定符

使用举例:

int main()
{
	printf("%d", Test::sqrt);
	return 0;
}

此时打印结果就是0。

💖注意点
  1. 在未注明命名空间时,使用的就是全局命名空间,就是我们引用的库函数中的命名空间,比如在上面的例子中,对sqrt进行打印就是打印的库函数中sqrt函数的地址的%d形式。

  2. 普通命名空间中不允许出现相同的变量名或者类型名或者函数名,因为它们是在同一个域内,在使用的时候指向不明确。
    具体细则如下:

  3. 变量名和函数名不能相同。

  4. 变量名或者函数名可以和类型名(结构体类型)相同
    当变量名或者函数名和类型名相同后程序员此时要承担相应的区别义务,比如

namespace Test
{
	int sqrt  = 0;
	void fun()
	{
		return;
	}
	struct sqrt
	{
		int b;
		char c;
	};
}
int main()
{
	Test::sqrt d;
	return 0;
}

像上面的这种方式去写,编译器会把Test::sqrt首先当作变量,而不是我们认为的结构体,这就是我们需要承担的义务,如果我们想要把它当作结构体,就必须在其前面加上一个struct才可以,让编译器将其看成是一个结构体,如果sqrt同时是函数名和结构体类型名的时候同样也是如此,这个地方需要无比注意。

  1. 在使用命名空间里的类型名的时候,struct如果要加上的话,只能加在命名空间名的前面。如下所示:
struct Test::sqrt d;//正确的方式
Test::sqrt struct d;//错误的方式
Test::struct sqrt d;//错误的方式
  1. 局部变量会屏蔽全局变量,我们想要直接使用全局变量而不适用局部变量的话,可以像下面这样进行书写:
int a = 0;
int main()
{
	int a = 1;
	printf("%d", ::a);
	return 0;
}

像这样,在a的前面加上一个::,使用的就是全局变量a,而不遵循局部优先原则。
分析原因:域作用限定符前面是空白,此时访问的是默认的域,默认的域是全局域。

  1. 可以定义多个命名空间,不同命名空间中可以存在相同的变量名或者函数名。
    例如下面的定义是合法的:
namespace Test
{
    int a = 0;
    void fun
    {
        return 0;
    }
}
namespace Test2
{
    int a = 0;
    void fun
    {
        return 0;
    }
}
  1. 虽然命名空间有着独立的域,但是我们无法对命名空间进行取地址操作,这种操作是非法的。
  2. 命名空间中的变量存放的位置位于全局静态区,和全局变量一样是在程序运行前就已经开辟好空间的。
  3. 命名空间中的类型在主函数中定义变量的时候需要加上它的命名空间名,但是后续使用的时候就不需要在变量名前面加上域名了。例如:
namespace stu
{
    struct class
    {
        int a;
        char c;
    }
}
int main()
{
    stu::class student = {1,'a'};
    printf("%d\n",student.a);
}
  1. 不同命名空间中可以定义相同的类型名,但是不可以对相同或者不同的类型名定义相同名字的变量。此时命名空间隔离的类型,而不是变量。例如下面这种操作是非法的:
namespace Test
{
    struct stu
    {
        int a;
        char c;
    }
}
namespace Test2
{
    struct stu
    {
        int a;
        char c;
    }
}
//上面的两种命名空间的定义是合法的
int main()
{
    Test::stu student1 = {1,'a'};
    Test2::stu student1 = {2,'b'};
    return 0;
}

这种情况出现了student1重定义的现象,为什么呢?因为变量名本质上标识的是地址,不能在同一个地址上定义两个变量。

  1. 命名空间中定义的变量本质上还是全局变量,生命周期和全局变量是类似的
  2. 命名空间只能在定义全局变量的位置进行定义,不能在函数内进行定义。在工程中,命名空间一般是在.h头文件中进行定义。例如下面的这种定义形式:
int main()
{
    namespace stu
    {
        int a = 0;
    }
    return 0;
}
  1. 头文件中不应该包含using声明。

这是因为头文件头文件的内容会拷贝到所有引用它的文件中去,如果头文件里有某个using声明,那么每个使用了该头文件的文件都会有这个声明。对于某些程序来说,由于不经意间包含了一些名字,反而可能产生始料未及的名字冲突,所以一般using声明一般使用在.cpp文件中。

💘3.1.2 命名空间的嵌套定义

命名空间的嵌套定义是为了防止同一个命名空间中的变量、函数名或者类型名发生冲突。

💖嵌套命名空间的定义和使用

定义:

namespace school
{
    namespace class
    {
        int a = 0;
    }
}

使用:

int main()
{
    printf("%d",school::class::a);
}
💖注意点
  1. 同名的命名空间是可以同时存在的,编译器编译时会进行合并。例如下面是等价的:
namespace stu
{
    int a = 0;
}
namespace stu
{
    int b = 0;
}
//上面的两段命名空间定义和下面的一样
namespace stu
{
    int a = 0;
    int b = 0;
}
  1. 嵌套定义的命名空间内部的变量或者函数名和外部的变量或者函数名是可以相同的,这也是它出现要解决的问题所在。例如下面的操作是合法的:
namespace stu
{
	int a = 0;
	void fun()
	{
		return;
	}
	struct n
	{
		int c;
		char d;
	};
	namespace N1
	{
		int a = 1;//使用:stu::N1::a
		void fun()//使用:stu::N1::fun()
		{
			return;
		}
		struct n//使用:stu::N1::n
		{
			int m;
			char n;
		};
	}
}

💘3.1.3 命名空间的引入

💖命名空间引入的使用

格式:

using namespace 命名空间名;

使用举例:

假设上面的命名空间的定义已经出现在了另一个.h头文件中,那么我们可以在.c文件中这样引入:

using namespace stu;//引入stu命名空间
using namespace N1;//引入命名空间N1

引入命名空间后我们可以直接使用stu中定义的变量、函数和类型,但是有许多需要注意的地方:

💖注意点
  1. 当我们如上面的例子中,引入了stu命名空间后,此时如果直接使用a的话使用的就是0,而不是1,如果我们想要使用N1中的a,我们就必须这样进行使用:stu::N1::a

  2. 如果我们同时展开了上面的stu和N1,此时就不能直接使用变量a了,因为指向不明确,编译器不知道是stu里面的还是N1里面的,此时如果想要使用a的话,必须明确前面的域,比如stu::astu::N1::a

  3. 命名空间的引入也是有顺序的。如果我们想要引入N1,我们有两种写法

using namespace stu;
using namespace N1;//此时必须要有前面的那一行代码,不然会找不到N1
using namespace stu::N1;
  1. 可以用什么引入什么,只引入某个命名空间的变量或者函数或者类型。例如:
using stu::a;
using stu::fun();
using stu::n;

💖3.2 命名空间的使用

namespace N
{
	int a = 10;
	int b = 20;
	int Add(int left, int right)
	{
		return left + right;
	}
	int Sub(int left, int right)
	{
		return left - right;
	}
}
int main()
{
	printf("%d\n", a); // 该语句编译出错,无法识别a
	return 0;
}

三种使用方式:

  • 加命名空间名称及作用域限定符
int main()
{
 printf("%d\n", N::a);
 return 0; 
}
  • 使用using将命名空间中成员引入
using N::b;
int main()
{
 printf("%d\n", N::a);
 printf("%d\n", b);
 return 0; 
}

注意:

我们一般会使用这种方式:

using std::cout;

这样引入一些常用的,因为直接引入一个命名空间会造成命名污染,容易出现重定义的现象。

  • 使用using namespace 命名空间名称引入
using namespce N;
int main()
{
 printf("%d\n", N::a);
 printf("%d\n", b);
 Add(10, 20);
 return 0; 
}

💖3.3 对std的解释

==std是封C++库的命名空间。==比如cout和cin就是封在标准命名空间中的。

如果我们不进行引入,那么我们只能像下面这样进行使用:

int main()
{
	int a = 0;
	std::cout << a << std::endl;
}

在封装到标准命名空间的名字前加上std::。

🧡3. C++输入&输出

我们来看一个程序:

#include<iostream>
using namespace std;
int main()
{
	int a = 0;
    cin >> a;
    cout << a;
	return 0;
}

运行结果:

说明:

  1. 使用cout标准输出(控制台)cin标准输入(键盘)时,必须包含< iostream >头文件以及std标准命名空间。
    注意:早期标准库将所有功能在全局域中实现,声明在.h后缀的头文件中,使用时只需包含对应头文件 即可,后来将其实现在std命名空间下,为了和C头文件区分,也为了正确使用命名空间,规定C++头文件不带.h;旧编译器(vc 6.0)中还支持格式,后续编译器已不支持,因此推荐使用<iostream> +std的方式。
  2. 使用C++输入输出更方便,不需增加数据格式控制,编译器能够自动识别类型,比如:整形–%d,字符–%c
#include <iostream>
using namespace std;
int main()
{
	int a;
	double b;
	char c;

	cin >> a;//一次输入一个数据
	cin >> b >> c;//一次可以输入多个数据,默认以空格或者换行进行分割

	cout << a << endl;//输出变量a中存储的值和endl(换行符)
	cout << b << " " << c << endl;//输出变量b的值和空格和变量c的值还有换行符,说明一次可以输出多个数据
	return 0;
}

和C语言不同的是:

无论是输入还是输出,我们都不需要指定相应的类型,编译器会自动进行类型识别和转换。

  1. 在C++中,>>是流提取运算符,<<是流插入运算符。
cin >> a;//从cin(键盘)输入数据,然后数据被提取到了变量a中,这就是C++中变量的输入
cout << a << endl;//将a插入到标准输出控制台(一般是显示器)中去了

🧡4. 缺省参数

C++中函数的参数也可以配备胎。

💖4.1 缺省参数概念

缺省参数是声明或定义函数时为函数的参数指定一个默认值。在调用该函数时,如果没有指定实参则采用该默认值,否则使用指定的实参。

void TestFunc(int a = 0)
{
	cout << a << endl;
}
int main()
{
	TestFunc(); // 没有传参时,使用参数的默认值,输出结果为0
	TestFunc(10); // 传参时,使用指定的实参,输出结果为10
}

💖4.2 缺省参数分类

  • 全缺省参数
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(5);//输出结果为5 20 30 
    TestFunc(5,6);//输出结果为5 6 30
    TestFunc(5,6,7);//输出结果为5 6 7
    return 0;
}

注意:C++中不支持下面的语法:

TestFunc(,5,6);
  • 半缺省参数(在使用函数时至少传一个)
void TestFunc(int a, int b = 10, int c = 20)
{
	cout << "a = " << a << endl;
	cout << "b = " << b << endl;
	cout << "c = " << c << endl;
}

注意:

  1. 半缺省参数必须从右往左依次来给出,不能间隔着给
  2. 缺省参数不能在函数声明和定义中同时出现(即使值相同也不行,编译器会显示重定义默认参数)
//a.h
void TestFunc(int a = 10);
// a.c
void TestFunc(int a = 20)
{}
// 注意:如果声明与定义位置同时出现,恰巧两个位置提供的值不同,那编译器就无法确定到底该用那个缺省值。

注意:此时又会出现两种情况:

1、声明给默认参数,定义不给默认参数。如下所示:

void fun(int a = 20);
void fun(int a)
{
	cout << a << endl;
}

int main()
{
    fun();//输出结果为20
    fun(10);//输出结果为10
    return 0;
}

2、声明不给默认参数,定义给默认参数。如下所示:

void fun(int a);
void fun(int a = 20)
{
	cout << a << endl;
}
int main()
{
    fun();//程序无法正常运行,函数不接受0个参数
    //原因:在链接之前,各个cpp文件会生成.obj文件,如果在声明不给,.h就会在源文件中展开,程序在编译时程序无法找到它的默认参数,程序只有在链接的时候才会找对应函数的地址,才能知道它的默认参数,即编译阶段只能拿到声明,无法拿到定义,自然无法知道定义中的默认参数
    fun(10);//输出结果为10
    return 0;
}
  1. 缺省值必须是常量或者全局变量
  2. C语言不支持(编译器不支持)

🧡5. 函数重载

💖5.1 函数重载概念

函数重载:是函数的一种特殊情况,C++允许在同一作用域中声明几个功能类似的同名函数,这些同名函数的形参列表(参数个数或类型或 顺序(不同类型的形参))必须不同,常用来处理实现功能类似数据类型不同的问题。

函数重载的意义:

函数的名字仅仅是让编译器知道它调用的是哪个函数,而函数重载可以在一定程度上减轻程序员起名字,记名字的负担。

注意:main函数不能重载!

注意:C语言不支持同名函数,只要名字相同就属于重定义。

int Add(int left, int right)//函数1
{
	return left + right;
}
double Add(double left, double right)//函数2
{
	return left + right;
}
int Add(int a,int b,int c)//函数3
{
    return a + b + c;
}
void Print(int a,double c)//函数4
{
    cout << a << endl;
    cout << c << endl;
}
void Print(double c,int a)//函数5
{
    cout << a << endl;
    cout << c << endl;    
}
int main()
{
	Add(10, 20);
	Add(10.0, 20.0);
	Add(10, 20, 30);
	return 0;
}

(1)函数1和函数2属于参数类型不同

(2)函数1和函数3属于参数个数不同

(3)函数4和函数5属于参数顺序不同

问:为什么返回值无法作为函数重载的条件?

答:我们在调用函数时使用的仅仅是函数名和实参,并不涉及到返回值,所以返回值不同无法作为函数重载的条件。

💖5.2 名字修饰

为什么C++支持函数重载,而C语言不支持函数重载呢?

在C/C++中,一个程序要运行起来,需要经历以下几个阶段:预处理、编译、汇编、链接

C++支持重载而C语言不支持重载的原因发生在它们的链接上,如下例所示:(linux下)

(1)C++情况下:

下面有三个源文件

f.cpp

#include"f.h"
void f(int a, double b)
{
	printf("%d %lf", a, b);
}
void f(double a, int b)
{
	printf("%lf %d", a, b);
}

f.h

#include<stdio.h>
void f(int a,double b);
void f(double a, int b);

test.cpp

#include"f.h"
int main()
{
	f(1,3.14);
	f(3.14,1);
	return 0;
}

汇编的时候会生成符号表(.obj文件)(函数名和函数地址形成的映射)

f.cpp生成的符号表:

函数符号标识地址
_Z1fid0xffffff11
_Z1fdi0xffffff22

test.cpp生成的符号表:

函数符号标识地址
主函数(main函数)0x11223344
_Z1fid?
_Z1fdi?

在链接的时候test.cpp形成的符号表会和f.cpp形成的符号表进行链接,链接之后两个?都会成功被填充,所以此时可以成功链接。

注意:上面的i说明第一个参数类型是int,d说明第二个参数类型是double,1说明函数名只有1个字符,即f。

(2)C情况下:

下面有三个源文件

f.c

#include"f.h"
void f(int a, double b)
{
	printf("%d %lf", a, b);
}
void f(double a, int b)
{
	printf("%lf %d", a, b);
}

f.h

#include<stdio.h>
void f(int a,double b);
void f(double a, int b);

test.c

#include"f.h"
int main()
{
	f(1,3.14);
	f(3.14,1);
	return 0;
}

汇编的时候会生成符号表(函数名和函数地址形成的映射)

f.c生成的符号表:

函数符号标识地址
f0xffffff11
f0xffffff22

test.c生成的符号表:

函数符号标识地址
主函数(main函数)0x11223344
f?
f?

在链接的时候test.c形成的符号表会和f.c形成的符号表进行链接,此时两个同名的f无法正常链接,这就是C语言不支持函数重载的原因。

当然,在链接的时候不止有这些,像链接的过程中还有静态库/动态库的调用,此时我们讨论下面的问题:

C语言能够调用C语言的静态库/动态库,C能够调用C的静态库/动态库,那么C语言能够调用C的静态库/动态库?C能够调用C语言的静态库/动态库?下一节就是!

💖5.3 extern “C”

有时候在C工程中可能需要将某些函数按照C的风格来编译,在函数前加extern “C”,意思是告诉编译器, 将该函数按照C语言规则来编译。比如:tcmalloc是google用C实现的一个项目,他提供tcmallc()和tcfree 两个接口来使用,但如果是C项目就没办法使用,那么他就使用extern “C”来解决。

下面将一步步带大家来在VS2019中模拟实现:

首先创建一个项目,和平时的一样,此处起名叫AddC.lib,然后创建Add.c源文件和Add.h头文件。创建好如下图所示:

此时我们准备生成静态库:

右击项目,点击属性。

改变配置类型,将应用程序改为静态库,然后点击确定。

此时点击生成解决方案。

此时可以看到已经生成.lib文件。

来到项目文件目录,点击Debug文件夹。

此时能够看到对应生成的.lib文件。

此时我们有一个C++的项目,我们想要使用上面的AddC.lib静态库。

首先在新建项目中包含刚才静态库的头文件,此处是采用#include路径的方式,当然,我们也可以直接将.h头文件复制到当前项目的文件夹下然后直接将其添加到左侧的头文件选项中,然后在我们当前项目源文件中用#include指令直接进行包含。

==注意:此时我们仍然是无法运行的,在编译的时候可以正常通过,但是链接的时候会出现问题,因为程序在链接的时候出现了问题,无法找到对应的Add函数。==此时我们还需要进行配置。

右击项目,点击进入属性选项。在左侧的目录中点击链接器,点击常规。然后点击附加目录,点击向下的箭头,点击编辑。

先点击黄色文件夹图标,然后点击三个点标志。

接下来将建立静态库项目的Debug文件夹选择。

选择之后点击确定。

点击左侧的输入,在附加依赖选项中添加AddC.lib,用分号和其它文件进行分割,然后点击确定。

此时我们已经成功配置好了,运行,发现无法正常运行,会出现链接错误,如下图所示:

此时我们再将静态库Add.c文件改为Add.cpp文件,发现程序能够正常运行输出2,为什么会发生这种情况呢?

因为我们.c文件在生成符号表的时候,和cpp文件是不一样的,.c文件不会带函数参数的类型,但是.cpp文件会带函数参数的类型,而我们的测试项目是.cpp文件,如果静态库文件是.c文件当然是不可以的,此时属于.cpp调用.c的静态库,而我们改变后缀后就是.cpp项目调用.cpp的静态库,此时是正常的,这就是上述情况出现的原因。

那么,如果我们.cpp文件想要调用.c的静态库,我们该如何实现?

我们在Test.cpp文件中#include的前面加上一个extern ”C“即可,告诉C++编译器:这个头文件中声明的函数是C库,要用C的方式去链接调用。

代码修改后如下图所示:

注意:只有extern ”C“,没有extern “C++”。

经过上面的修改后,即使AddC静态库中的文件是Add.c此时也能够正常调用并输出正确结果2了。
当然,上面的配置撕毁太过麻烦了,我们想要简单一些,可以吗?

答案是可以的,在上面的例子中,我们可以将我们生成的AddC.lib静态库直接将其复制粘贴到我们Test项目的目录下,具体操作如下所示:

首先找到AddC.lib文件,右击复制。

粘贴在Test的项目的目录下。

经过粘贴过去之后我们发现,我们这样操作之后,同样也可以直接使用,而且不需要去对前面的进行配置,同理,我们也可以直接将Add.h文件直接复制粘贴到Test项目的目录下,那样我们就不需要在包括头文件的时候加上路径,总而言之,两种方法各有优缺点。

问:C能否调用C++的静态库呢?

答:可以的。在上面的示例中,我们只需要在静态库项目中Add.h中进行这样的修改即可:

然后将我们Test.c的文件修改成C语言的形式:

然后运行,即可输出2,说明此时能够正常运行。

或者采用下面这种修改的方式:

extern “C”有两种用法:

1、直接加在函数声明的前面

extern "C" void f1(int a,int b);
extern "C" void f2(int a,int b);

2、将函数声明用一个大括号括起来,然后在前面加上

extern "C"
{
	void f1(int a,int b);
	void f2(int a,int b);
}

注意:C在调用C的静态库的时候,无法使用带有函数重载的静态库,在编译的时候就无法通过。在.h头文件进行展开的时候就发现了函数重定义。如果仍然想调用,就必须将C库中函数名字修改一下,使其不再重载。但这样就失去了重载的意义。

相关文章