C++基础入门

0.前言

你好这里是limou3434的C++博文系列,对于C++,我计划出一个学习专栏,预计在8月份初步完成,这些都是我学习C++时的一些体会,能被您所任用真的是太好了。

感兴趣的话,您还可以看看我的其他博文系列。

本次我给您带来的内容主要是C++的一些基础知识,如果您学过一门语言(最好是C语言)学起来就会轻松一些。本文主要是指出一些和C语言编程中有比较大不同的地方。

  1. C++是在C语言的基础上,容纳进面向对象编程语言的思想,并且增加了许多有用的库,以及编程范式等
  2. 在学习C++语言后,可以明白C语言设计不合理的地方,明白面向对象编程是怎么一回事

1.C++关键字

C++比C语言多了很多关键字,具体可以到菜鸟教程粗略看一看了解一下。

2.命名空间

2.1.问题隐患

在C++中,变量和函数和类都是大量存在的,这些名字都会存储在全局作用域中,因此在使用的时候可能导致很多的冲突.

2.2.解决方案

  • 使用命名空间可以对标识符的名字进行本地化,避免造成命名冲突或名字污染,namespace就是针对这个问题的,而C语言没有办法解决这个问题(例如将库函数名字作为新定义的变量)
  • C++允许同一个工程存在多个同名称的命名空间,编译器最后会合并成同一个命名空间里
  • C++为了防止名字的冲突,便把标准库的东西都放入std这个命名空间。这样就可以使用标准库的名字来命名自己的变量和函数,因此要使用标准库的名字也需要写出它的命名空间

2.3.namespace的使用例子

namespace limou3434//后面是这块命名空间的名字
{
    int print = 100;//在命名空间内定义一个变量
    int function(int n)//在命名空间内定义一个函数
    {
        return n + 1;
    }
    struct Limou//在命名空间内定义一个结构体
    {
        int a;
        char b;
        float c;
        double d;
    };
    namespace limou//在命名空间内嵌套一个命名空间
    {
        int e = 1;
        int f = 2;
        int g = 3;
    }
}
int main()
{
    return 0;
}

2.4.使用命名空间的成员

2.4.1.使用作用域限定符“::”单独引用

//命名空间名称::命名空间内的成员名字;
#include 
namespace limou3434//后面是这块命名空间的名字
{
    int print = 100;//在命名空间内定义一个变量
}
int main()
{
    printf("%d\n", limou3434::print);
    return 0;
}

2.4.2.使用关键字using将命名空间的某个成员引入(项目经常使用)

//using 命名空间名称::命名空间内成员名字;
#include 
namespace limou3434//后面是这块命名空间的名字
{
    int print = 100;//在命名空间内定义一个变量
}
using limou3434::print;
int main()
{
    printf("%d\n", limou3434::print);
    return 0;
}

2.4.3.使用关键字using和namespace将命名空间的整体引入

这样写不太好,这样会把所有标准库的名字全部暴露

using namespace limou3434;
using namespace std;//这里说明cout是std这块命名空间的成员之一
int main()
{
    cout << print;
    return 0;
}

3.C++输入和输出

3.1.代码例子

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

3.2.代码解说

  1. 使用cout和cin必须包含头文件
  2. 使用cout和cin必须指出其命名空间使用方法为std
  3. <<是流插入运算符,>>是流提取运算符(也有叫“输入输出符号”的)
  4. C++的输入输出可以自动识别变量的类型,无需再指定输入输出格式
  5. 这里的cout和cin分别是ostream和istream类型的对象,>>和<<也涉及到运算符重载的知识,还涉及到IO流的用法,不过这些都是后话
  6. 早期标准库将所有功能都在全局域中实现,声明在.h的头文件中,使用的时候include一下就行,后来将这些实现改到std命名空间下,为了和头文件区分并且正确使用命名空间,规定C++头文件不带.h
  7. 有的老旧编译器还支持#include的形式,而后续的编译器大部分都不再支持
  8. 虽然是自动控制,但是cout和cin还有其他复杂用法,包括控制输出格式等等,但是这些对比后续学习内容不算重点,就先不在此描述(而且C++也依旧支持C的printf和scanf等库函数)
  9. 另外,std是C++标准库的命名空间,在日常练习中虽然可以直接将整个命名空间全部暴露,但是在大型工程中,一般都会使用作用域限定符,只暴露某一成员

4.缺省参数

4.1.缺省函数的使用

声明或定义函数的时候为函数指定一个缺省值(默认值,这里是翻译问题,才叫缺省)。如果在使用函数的时候没有指定实参,就使用默认值为函数参数,否者使用指定的实参

#include
using namespace std;
void function(int a = 0)
{
    if (a == 0)
    {
        cout << "你好!!!" << endl;
    }
    else
    {
        cout << "hello!!!" << endl;
    }
}
int main()
{
    function();
    function();
    function(1);
    return 0;
}

4.2.“全缺省函数”和“半缺省函数”的使用

  • 可以只指定一部分参数有默认值,或者全部指定有默认值
  • 半缺省函数的参数必须“从右往左”给出,不可间隔给予默认值
  • 缺省函数不能在函数声明和定义里同时出现,要以声明为准
//.h文件 
voide function(int a = 100);
//.cpp文件 
void function(int a = 50) 
{
	//某些具体代码
}
//在VS2022中哪怕重定义的缺省的值是一样的也不行
#include 
using namespace std;
void fun(int a = 0);
int main()
{
	fun();
	return 0;
}
void fun(int a = 0)
{
	if (a == 0)
	{
		printf("0\n");
	}
	else
	{
		printf("%d\n", a);
	}
}
  • 另外C++没有允许使用“(,1)”这类的写法
  • 之前写的函数可以被缺省的做法优化,比如“栈的初始化函数”中,开辟空间只开了一点,但是我们明明知道要用的栈元素比较多,我们还是要从较少的几个元素开始,一点一点申请空间,这是有较大的消耗的,而缺省就可以优化这一点

5.函数重载

在现实生活中有的词语可以根据上下文语义,从而产生不同的意思,这就是一种重载的体现

5.1.函数重载

  • 在C++中允许在同一个“作用域”中声明几个类似功能的同名函数,这些函数的形参列表(形参个数、形参类型、形参顺序)是不同的,常常用来处理实现功能类似、数据不同的问题。而且也根据这个形参的使用来区分不同重载的函数
  • 需要注意的是:返回值的不同、形参名字的不同都不构成函数重载
  • 实际上cout、cin能够自动识别的本质也是函数重载,在库里面已经帮我们实现好了
#include
using namespace std;
void function(int a = 0)
{
    if (a == 0)
    {
        cout << "你好!!!" << endl;
    }
    else
    {
        cout << "hello!!!" << endl;
    }
}
char function(char b)
{
    cout << b << endl;
    return 1;
}
int main()
{
    function();
    function(1);
    function('c');
    return 0;
}
  • 为什么重载在C语言不支持,而C++语言却支持?(以下现象只有在汇编代码中才能看出来)
    • 要明白这个过程最主要在于链接
      • 预处理:头文件展开、宏替换、条件编译、去掉注释
      • 编译:语法检查、生成汇编代码
      • 汇编:将汇编代码转化为二进制机器码,生成目标文件
      • 链接:将程序合起来,生成可执行程序(这个合起来的意思就是将只给声明的函数地址找到链接起来,如果找不到就是链接错误。而具体的找法就是为每个目标文件做一个符号表,通过符号表来寻找对应的地址)
    • 而在C语言就是在这个链接阶段时,查找符号表的时候,有两个同名函数符号且地址不同,这就发生了冲突。即:“C语言直接将函数名作为符号来对应函数地址”
    • 而C++语言在这个链接阶段时,函数符号是根据函数名和参数名来生成的,这样符号名字不一样,地址也不一样,就可以构成函数重载的用法。即:“C++语言将函数名和参数名等组合起来构成符号来对应函数地址,这也解释了为什么函数重载需要靠参数来标识不同的重载函数”
    • 因此综上所述,C语言不支持函数重载(没有函数修饰规则),C++语言支持函数重载(有函数修饰规则)
    • 另外,在C++这个符号表里的“函数修饰规则”在不同环境是有可能不一样的,但是一定是依赖函数参数来生成的
  • 引用在定义的时候就必须初始化
  • 一个变量可以有多个引用
  • 引用一旦引用一个实体,就再也不能引用其他实体
int a = 10;
int& b = a;

int x = 20;
b = x;//因此这个语句的意思是“x的值赋给b”,而不是“x成为了b的别名”,这跟指针就有很大的区别

5.2.重载原理

有关重载的原理这里,涉及到的C++内容比较多,以后我再来进行补充。

6.引用

6.1.引用概念

引用不是新定义一个变量,而是为已存变量取个别名,引用变量不会开辟新的内存空间,它和它引用的变量共用同一块内存空间。

6.2.语法格式

类型& 引用变量名(对象名) = 引用实体;
  • “引用”是给变量取别名,“typedef”是给类型取别名
  • 还可以给引用后的别名取别名
  • 引用更多是在函数的参数处使用,尤其是大对象传参的时候会提高效率
  • 引用还可以在函数的返回值处使用,但是注意函数内部定义的变量一旦出函数作用域就会被销毁,此时不能使用引用返回,只能使用传值返回(但是如果这个局部变量被静态关键字static修饰,那就可以直接使用引用返回)
  • 引用的变量类型和引用类型不一样时,会发生权限放大错误,本质是创建了临时变量,而临时变量具有常属性

6.3.具体代码

6.3.1.例一

#include 
int main()
{
    int a = 10;//实际变量
    int& a1 = a;//引用1
    int& a2 = a;//引用2
    printf("%d %d %d\n", a, a1, a2);
    printf("%p %p %p", &a, &a1, &a2);
    return 0;
}

6.3.2.例二

int main()
{
    const int a = 10;
    //int& a1 = a;
    const int& a1 = a;
    10;
    //int& b1 = 10;
    const int& b1 = 10;
    double c = 3.14159;
    //int& c1 = c;
    const int& c1 = c;
    return 0;
}

6.3.3.例三

void Swap(int& a, int& b)
{
  	int tmp = a;
  	a = b;
  	b = tmp;
}
int main()
{
	int x = 0, y = 2;
  	Swap(x, y);
}

6.3.4.例四

#include 
int& function(int& x)//int& x = i,因此x是i的别名
{
    x++;//这个x++等价于i++
    return x;//返回x,int& ("function()") = x,因此函数甚至可以被赋值
}
int main()
{
    int i = 0;
    int j = 0;
    j = function(i);
    printf("%d\n", j);
    printf("%d\n", function(i) = 10);
    return 0;
}

6.3.5.例五(很重要的一个例子)

//顺序表结构体
typedef struct SeqList
{
 	int* a;
	int size;
	int capacity;
}SL;
//初始顺序表
void SLPushInit(SL& s, int capacity = 4);
//尾插顺序表
void SLPushBack(SL& S, int x);
//修改顺序表
int& SLAt(SL& s, int pos);
{
	assert(pos >= 0 && pos <=s.size);
  	return s.a[pos];
}
int main()
{
	SL sl;
  	SLPushInit(sl);//初始化
  	SLPushBack(sl, 1);//尾插
	SLPushBack(sl, 2);//尾插
	SLPushBack(sl, 3);//尾插
	SLPushBack(sl, 4);//尾插
  	for(int i = 0; i < sl.size; ++i)
    {
    	cout << SLAt(sl, i) << endl;//输出顺序表的元素
    }
  	SLAt(sl, 0)++;//拿到s.a[pos],对其进行++
  	SLAt(sl, 0) = 10;//拿到s.a[pos],修改成10
}

6.3.6.例六(权限)

#include 
using namespace std;
int main()
{
	const int c = 20;//c可读不可写
	//int& d = c;//d把c权限放大了(可读可写),这是不被允许的
  	const int& d = c;//这是允许的,属于权限平移的概念
  
  	int e = 30;
  	const int& f = e;//但是权限缩小是被允许的
  
  	int g = 1;
  	double h = g;//这里产生一个临时变量,将存储数据提升后的g,再赋予h(这里g用显式强转也不行,无论是显式还是隐式,都不会改变g本身的类型)
  	//double& i = g;//这是不被允许的,因为这里产生一个临时变量,将存储数据提升后的g,而这个临时变量具有常属性,临时变量被h引用的话发生了权限放大
  	const double& i = g;//这是被允许的,只是发生了权限平移
  
 	const double& j = 3.14;//这个也是被允许的,因此拥有const修饰的引用允许引用常量。所以如果是在函数形参处使用引用时,如果不需要改变值,就要尽可能使用const修饰
  	return O;
}
  • 注意权限放大和缩小只适用在指针和引用上,普通变量之间的赋值是没有作权限要求的,因为这只是做一份临时拷贝
#include 
using namespace std;
void function_1(int n)
{
	;
}
void function_2(int& n)
{
	;
}
void function_3(const int& n) 
{ 
	;
}
int main()
{
	int a = 10;
	const int b = 20;

	function_1(a);
	function_1(b);
	function_1(30);

	function_2(a);
	//function_2(b);//这是不被允许的
	//function_2(30);//这是不被允许的

	function_3(a);
	function_3(b);
	function_3(30);

	return 0;
}

6.4.指针和引用的区别(两者是十分相似的)

指针更强大、更危险、更复杂/引用局限一些、更安全、更简单

  1. 概念:引用在概念上定义一个变量的别名,指针存储一个变量地址。
  2. 内存: 引用没有开空间,但是指针开了4个字节或是8个字节的空间
  3. 初始化:引用在定义时必须初始化,指针没有要求
  4. 重新赋值:引用在初始化时引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何一个同类型实体
  5. 空问题:没有NULL引用,但有NULL指针
  6. sizeof:在sizeof中含义不同:引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32位平台下占4个字节)
  7. 算术运算:引用自加,即引用的实体增加1;指针自加,即指针向后偏移一个类型的大小
  8. 嵌套使用:有多级指针,但是没有多级引用
  9. 访问实体方式:访问实体方式不同,指针需要显式解引用,引用编译器自己处理
  10. 安全性:引用比指针使用起来相对更安全

6.5.传值和传引用的效率对比

这里可以查看汇编代码,基本没有区别

7.内联函数

7.1.内联概念

  1. 内联关键字是inline,被其修饰的函数就叫内联函数
  2. 编译的时候,只要满足条件,C++编译器会在调用内联函数地方直接展开,没有函数调用建立栈帧的开销,内联函数能提升程序运行的效率
  3. 内联关键字的使用很像宏的使用
  4. 在debug下默认是不展开的,这里打开反汇编就会发现依旧是调用函数,这是为了方便调试,这是可以通过设置规避(“配置属性”-“C/C++”-“常规”-“调试信息格式”-“程序数据库(/Zi)”并且修改“配置属性”-“C/C++”-“优化”-“内联函数拓展”-“只使适用于 __inline(/Obl)”)

7.2.具体代码

#include
using namespace std;
inline int add(int a = 0, int b = 0)//被inline修饰的函数
{
    return a + b;
}
int main()
{
    int c = 0;
    c = add(3, 5);
    cout << c;
    return 0;
}

7.3.内联函数的反汇编(以后换Linux看)

这一部分在Windows不好演示,就等我后续用Linux再补充吧……

7.4.内联函数的特性

  • inline本质是一种空间换时间的做法,如果编译器将函数当成内联函数,在编译期间就会将函数体替换函数调用
    • 劣势:有可能使得目标文件变大
    • 优势:少了调用开销,提高程序的运行效率
  • inline类似C语言里的寄存器变量关键字,只是向编译器发出一个请求,不同编译器关于inline的实现机制可能不一样。
  • 一般建议在以下情况来使用inline:将函数规模小、非递归、非频繁调用的函数采用inline修饰,否则编译器有可能会忽略inline的特性(这是因为,函数内部代码指令如果比较长,有可能会让编译的程序暴增,导致编译产生的程序变大。这些更多取决于编译器对inline的实现和理解)
  • 另外被内联关键字修饰的函数,其声明和定义一般不建议分开写,分开会导致链接错误。因此最好是把内联函数的声明和定义直接一起写到头文件里,不要去做分离!

7.5.对比宏和inline

7.5.1.宏的优点

  1. 提高代码的复用性(让你的代码能适应更多种的情况,完成更多种情况的任务,这就是代码的复用性)代码的可维护性变强,修改某些常量值快捷方便
  2. 宏函数能提高效率,减少栈帧的建立

7.5.2.宏的缺点

  1. 不方便调试带有宏的代码(因为预编译阶段进行了替换,而调试是在编译后的)
  2. 导致代码的可读性差、有一点的复杂性,容易误用
  3. 没有类型安全的检查(替换机制),易出现类型错误

7.5.3.宏的代替方案

  1. 常量定义可以使用C++的const、enum
  2. 短小函数定义可以使用C++的内联关键字inline

7.6.约定

在现代C++中,基本建议尽量使用const、enum、inline,而不使用宏

8.“typedef关键字”和“auto关键字”

8.1.typedef关键字

随着一个工程的扩大,程序中用到的类型也越来越复杂,经常体现在

  • 类型难以拼写,容易拼错
  • 含义不明确,导致用错
/*没有使用tepedef重命名*/
#include 
#include 
int main()
{
    std::map<std::string,std::string>m{ { "apple", "苹果" }, { "orange", "橙子" },  {"pear", "梨"} };
    std::map<std::string, std::string>::iterator it = m.begin();
    //其中std::map::iterator是一个类型,都是类型的名字太长了,容易写错,可以尝试使用typedef给这个类型取个别名
    while (it != m.end())
    {
        //....
    }
    return 0;
}
/*没有使用tepedef重命名*/
#include 
#include 
int main()
{
    std::map<std::string,std::string>m{ { "apple", "苹果" }, { "orange", "橙子" },  {"pear", "梨"} };
    std::map<std::string, std::string>::iterator it = m.begin();
    //其中std::map::iterator是一个类型,都是类型的名字太长了,容易写错,可以尝试使用typedef给这个类型取个别名
    while (it != m.end())
    {
        //....
    }
    return 0;
}
  • 在编程时,常常需要把表达式的值赋值给变量,这就要求在声明变量的时候就要清楚知道表达式的类型。然而要做到这点并非那么容易,因此C++11给auto一个新的定义

8.2.auto关键字(C++11)

  • 在C++11的标准中,auto不再是存储类型说明符,而是一个新的类型指示符,来指示编译器
  • auto声明的变量必须由编译器在编译时期推导而得
  • 使用auto定义变量时必须对其进行初始化,在编译阶段编译器会根据初始化表达式来推导auto的实际类型。因此auto并非是一种“类型”的声明,而是一个类型声明时的“占位符”,编译器在编译期会将auto替换为变量实际的类型。
#include 
using namespace std;
int function()
{
    return 10;
}
int main()
{
    int a = 10;
    auto b = a;
    auto c = 'a';
    auto d = function();
    cout << typeid(b).name() << " " << b << endl;
    cout << typeid(c).name() << " " << c << endl;
    cout << typeid(d).name() << " " << d << endl;
    //auto e; 无法通过编译,使用auto定义变量时必须对其进行初始化
    return 0;
}
  • 结合指针和引用来使用
    • 结合指针的话,auto和auto*是没有区别的
    • 结合引用的话,auto和auto&是有区别的,必须要加&
#include 
using namespace std;
int main()
{
    //一个变量
    int x = 100;

    //结合指针
    auto a = &x;
    auto* b = &x;

    //结合引用
    auto& c = x;

    //测试类型和输出,typeid可以打印类型
    cout << typeid(a).name() << " " << a << endl;
    cout << typeid(b).name() << " " << b << endl;
    cout << typeid(c).name() << " " << c << endl;

    //修改变量
    *a = 10;
    *b = 20;
    c = 30;
    return 0;
}
  • 在同一行定义多个变量
    • 当在同一行使用auto来声明多个变量时,这些变量必须是相同的类型,否则编译器将会报错,因为编译器实际只对第一个类型进行推导,然后用推导出来的类型定义其他变量
#include 
using namespace std;
int main()
{
    //正确使用
    auto a = 1, b = 2;

    //错误使用
    auto c = 3, d = 4.0;//该行代码会编译失败,因为c和d的初始化表达式类型不同
    return 0;
}
  • 结合基于范围的for循环中使用auto
  • auto不能推导的场景
    • auto不能作为函数的形参,因为函数编译是要建立栈帧的,这个时候都不知道形参的大小,怎么知道从哪里开始创建栈帧呢?
    • auto不能用来声明数组
  • 为了避免和C++98的auto发生混淆,C++11只保留了auto作为类型指示符的用法
  • auto的最大的优势
    • 其实在C++11提供的新式for循环以及lambda表达式

9.基于范围的for循环

这不是C++的首创,而是后面加进来的

9.1.范围for的语法

//在C++98中,遍历一个数组可以按照以下的方式使用
#include 
using namespace std;
//使用C++98遍历方式
int main()
{
    int array[] = { 1, 2, 3, 4, 5 };
    for (int i = 0; i < sizeof(array) / sizeof(array[0]); ++i)
        array[i] *= 2;
    for (int* p = array; p < array + sizeof(array)/ sizeof(array[0]); ++p)
        cout << *p << endl;
}
//在C++11中可以使用基于范围的for循环。
//for后面的括号由冒号“:”分为两部分,第一部分是范围内用于迭代的变量,第二部分表示被迭代的范围
#include 
using namespace std;
//使用C++11遍历方式
int main()
{
    int array[] = { 1, 2, 3, 4, 5 };
    for(auto& e : array)
        e *= 2;
    for(auto e : array)
        cout << e << " ";
    return 0;
}

9.2.范围for的使用条件

由于C++不支持直接传数组(这样消耗大,浪费)所以在函数传数组的时候必须提供begin和end方法,begin和end就是for循环迭代的范围(有关begin和end的具体使用后面再说)

void function(int arr[])//这个函数是不正确的,因为for的范围不确定
{
	for(auto& e : arr)
    	cout << e << endl;
}

10.指针空值nullptr(C++11)

10.1.NULL的概念

//NULL实际是一个宏,在传统的C头文件(stddef.h)中,可以看到如下代码
#ifndef NULL
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif
#endif
//在C++98中,字面量0既可以是一个整型数字,也可以是无类型的指针(void*)常量,但是编译器默认情况下将其看作整型常量,如果这么使用NULL时,就会具有一定的麻烦
void f(int)
{
    cout<<"f(int)"<<endl;
}
void f(int*)//函数重载
{
    cout<<"f(int*)"<<endl;
}
int main()
{
    f(0);
    f(NULL);//误用第一个函数,因为处理NULL的时候,NULL是被定义为0的
    f((int*)NULL);//需要使用强制类型转换才可以使用第二个函数
    return 0;
}

10.2.nullptr的概念

  • 而在使用nullptr表示指针空值时,不需要包含头文件,因为nullptr是C++11作为新关键字引入的
  • 在C++11中,“sizeof(nullptr)”与“sizeof((void*)0)”所占的字节数相同
  • 为了提高代码的健壮性,在后续表示指针空值时建议最好使用nullptr

11.打印数据类型

在C++中,可以使用函数来打印一个变量的类型

#include 
using namespace std;
int main()
{
	int a = 10;
	double b = 3.14;
	cout << typeid(a).name() << endl;
	cout << typeid(b).name() << endl;
	return 0;
}

这个了解一下就行,以后我还会在带您仔细研究这个打印数据类型的原理的。

你可能感兴趣的:(C++学习笔记,c++,开发语言,算法)