《解锁 C++ 基础密码:输入输出、缺省参数,函数重载与引用的精髓》

🔥个人主页:@草莓熊Lotso

🎬作者简介:C++研发方向学习者

📖个人专栏:************************************************************************************************************************************************************************************************************************************************************《C语言》《数据结构与算法》《C语言刷题集》《Leetcode刷题指南》****************************************************************************************************************************************************************************************************************************************************************

⭐️人生格言:生活是默默的坚持,毅力是永久的享受。

前言:在上篇博客中我们简单了解了C++的发展历史以及命名空间的使用这些知识点,但我们的C++入门基础知识还有不少,今天主要是想给大家分享一下C++的输入输出,缺省函数,重载以及引用的部分知识点


目录

一.C++的输入和输出

关键要点:

举例说明:

二.缺省参数

关键要点:

举例说明:

三.函数重载

三种构成重载的一般情况:

两种特殊情况,一个可以(但是存在一定问题),一个不行:

四.引用

引用的概念和定义:

引用的特性:

举例说明: (注意看注释)

引用的使用:

举例说明:(注意看注释)

传值返回,传引用返回:

传值返回:(主要看错误的地方)

传引用返回:(主要看错误的地方)

两个小问题:

传引用返回的正确使用:(顺序表为例)


一.C++的输入和输出

关键要点:

  • <iostream> 是 Input Output Stream 的缩写,是标准的输⼊、输出流库,定义了标准的输入、输出对象。
  • std::cin 是 istream 类的对象,它主要面向窄字符(narrow characters (of type char))的标准输入流。
  • std::cout 是 ostream 类的对象,它主要面向窄字符的标准输出流。
  • std::endl 是一个函数,流插入输出时,相当于插入一个换行字符加刷新缓冲区'。
  • <<是流插入运算符,>>是流提取运算符。(C语言还用这两个运算符做位运算左移/右移)
  • 使用C++输入输出更方便,不需要像printf/scanf输入输出时那样,需要手动指定格式,C++的输入输出可以自动识别变量类型(本质是通过函数重载实现的,这个以后会讲到),其实最重要的是C++的流能更好的支持自定义类型对象的输入输出。
  • IO流涉及类和对象,运算符重载、继承等很多面向对象的知识,这些知识我们还没有讲解,所以这里我们只能简单认识⼀下C++ IO流的用法。
  • cout/cin/endl等都属于C++标准库,C++标准库都放在一个叫std(standard)的命名空间中,所以要通过命名空间的使用方式去用他们。
  • ⼀般日常练习中我们可以using namespace std,实际项目开发中不建议using namespace std。
  • 这里我们没有包含<stdio.h>,也可以使用printf和scanf,在包含<iostream>间接包含了。vs系列编译器是这样的,其他编译器可能会报错。

举例说明:

cpp 复制代码
#include<iostream>
#include<algorithm>
using namespace std;

int main()
{
	int a, b;
	cin >> a >> b;//输入

	cout << a << " " << b << '\n';//输出
	cout << a << " " << b << endl;//end line

	return 0;
}

--用'\n'的效率会更高,除此以外,cout和cin的效率其实也是不如printf和scanf的,但是我们可以通过取消同步流的操作来解决这个问题,代码如下

cpp 复制代码
#include<iostream>
using namespace std;

int main()
{
	//取消同步流
	//在io需求比较高的地方,比如需要大量输入的竞赛题中,加上以下3行代码,可以提高效率
	ios_base::sync_with_stdio(false);
	cin.tie(nullptr);
	cout.tie(nullptr);
}

二.缺省参数

关键要点:

  • 缺省参数是声明或定义函数时为函数的参数指定⼀个缺省值。在调用该函数时,如果没有指定实参则采用该形参的缺省值,否则使用指定的实参,缺省参数分为全缺省和半缺省参数。(有些地方把缺省参数也叫默认参数)
  • 全缺省就是全部形参给缺省值,半缺省就是部分形参给缺省值。C++规定半缺省参数必须从右往左依次连续缺省,不能间隔跳跃给缺省值。
  • 带缺省参数的函数调用,C++规定必须从左到右依次给实参,不能跳跃给实参。
  • 函数声明和定义分离时,缺省参数不能在函数声明和定义中同时出现,规定必须函数声明给缺省值。

--上面的这些定义在后面都会给出对应举例,注意看注释

举例说明:

--包含全缺省和半缺省,需要注意的地方,注释都讲的比较清楚

cpp 复制代码
#include<iostream>
using namespace std;

void func(int a = 0)
{
	cout << a << '\n'<<'\n';
}

//全缺省
void func1(int a = 10,int b = 20,int c = 30)
{
	cout << "a= " << a << '\n';
	cout << "b= " << b << '\n';
	cout << "c= " << c << '\n'<<'\n';
}

//半缺省
//从右至左依次缺省才可以,中间不能跳跃缺省,不能出现 int a=10,int b=20,int c或int a=10,int b,int c=30等这种情况
void func2(int a , int b , int c=30)//如果非要a缺省的话,我们可以把a和c的位置换一下
{
	cout << "a= " << a << '\n';
	cout << "b= " << b << '\n';
	cout << "c= " << c << '\n'<<'\n';
}

int main()
{
	func(1);//1,传参时,使用的是指定的实参
	func();//没有传参会使用缺省值

	//以下4种都可以
	func1(1, 2, 3);
	func1(1, 2);
	func1(1);
	func1();
	//这种不行,不能跳跃传参
	/*func1(1, , 3);*/

	func2(1, 2, 3);
	func2(1,2);
	return 0;
}

--除此以外,缺省参数在改进数据结构中也使用的比较多,我们这里以顺序表这个结构的几个接口来看看,中间的实现我会比较省略,详细的结构实现可以去看一下博主之前的博客,说明一下,像顺序表这个数据结构的.cpp和.h我简单省略的实现后,就给大家展示下图片好了。

SeqList.cpp:

SeqList.h:

test.c:(注意看注释)

cpp 复制代码
#include<iostream>
#include"SeqList.h"
using namespace std;

int main()
{
	int n;
	cin >> n;
	SL s;
	SLInit(&s, n);//已经知道n的大小
	//如果没有输入操作这里n就是不知道的,就会使用缺省的n=4

	//为啥要定义这么一个n呢
	//比如我要尾插1000个数据,那么不用这个的话,就要不停的扩容,会有消耗
	for (int i=0;i<n;i++)
	{
		SLPushBack(&s, i);
	}

	//Find
	//原来的find无法实现找顺序表中所有需要找的元素(重复的只会返回第一个)
	//改了之后,通过下述操作可以找到所有的
	//  5 4 6 3 4 7 4
	// // 查找出所有的4
	int x = SLFind(&s, 4);
	while (x != -1)
	{
		x = SLFind(&s, 4, x + 1);
	}
	return 0;
}

三.函数重载

我们知道在C语言中不支持同一作用域中出现同名函数。但是C++是支持的,不过要求这些同名函数的形参不同,可以是参数个数不同或者类型不同。这样C++函数调用就表现出了多态的行为,使用更加灵活

三种构成重载的一般情况:

1.参数类型不同:

cpp 复制代码
//1.函数参数类型不同
#include<iostream>
using namespace std;

int Add(int left, int right)
{
	cout << "int Add(int left, int right)" << endl;
	return left + right;
}

double Add(double left, double right)
{
	cout << "double Add(double left, double right)" << endl;
	return left + right;
}

int main()
{
	cout << Add(1, 2) << '\n';
	cout << Add(1.1, 2.2) << '\n';
	//自动识别函数传的参数类型,调用不同的函数;
	return 0;
}

2.参数个数不同:

cpp 复制代码
//2.函数参数个数不同
#include<iostream>
using namespace std;

void f1()
{
	cout << "f1()" << '\n';
}

void f1(int a)
{
	cout << "f1(int a)" << '\n';
}

int main()
{
	f1();
	f1(1);
	//根据传参的个数,自动识别调用对应函数
	return 0;
}

3.参数类型顺序不同:

cpp 复制代码
//3.函数类型顺序不同--本质上还是对应的函数参数类型不同
#include<iostream>
using namespace std;

void f1(double a,int b)
{
	cout << "f1(double a,int b)" << '\n';
}

void f1(int a,double b)
{
	cout << "f1(int a,double b)" << '\n';
}

int main()
{
	f1(2.3,1);
	f1(1,2.3);
	//根据传参的顺序来调用对应函数
	
	return 0;
}

两种特殊情况,一个可以(但是存在一定问题),一个不行:

无法构成重载:(函数的仅仅只是返回类型不同,无法构成重载)

cpp 复制代码
//1.无法构成重载
#include<iostream>
using namespace std;

void fork()
{
}

int fork()
{
	return 1;
}

int main()
{
	//调用时无法确定调用那个
	fork();
	int x = fork();
	return 0;
}

可以构成重载,但是调用时会出现歧义:(缺省的情况)

cpp 复制代码
//2.可以构成重载
//但是一些情况下调用存在歧义
#include<iostream>
using namespace std;

void f1()
{
	cout << "f()" << endl;
}

void f1(int a = 10)
{
	cout << "f(int a)" << endl;
}

int main()
{
	f1(1);//这个可以
	f1();//这样调用就不行了,不确定调用那一个
	return 0;
}

四.引用

引用的概念和定义:

引用并不是定义一个变量,而是给已存在变量取了一个别名,编译器不会为引用变量开辟内存空间,它和它的引用变量共用同一块内存空间。

  • 类型& 引用别名 = 引用对象

在C++中为了避免引入太多的运算符,会复用C语言的一些符号,比如前面的<<和>>,以及这里的&可以是取地址,引用还可以是按位与运算符,特别容易混淆,大家需要注意区分的角度。

引用的特性:

  • 引用在定义时必须初始化
  • 一个变量可以有多个引用
  • 引用一旦引用⼀个实体,再不能引用其他实体

举例说明: (注意看注释)

cpp 复制代码
#include<iostream>
using namespace std;

int main()
{
	int i = 1;
	int& j = i;//j是i的别名

	//一个变量可以有多个别名,引用。别名也可以有它的别名
	int& k = j;
	k++;//k的变化会影响i和j

	cout << &i << '\n';
	cout << &j << '\n';
	cout << &k << '\n';
	//打印发现地址一样

	//引用时必须先初始化
	//int& x;//看报错
	//x = i;

	// 引用一旦引用一个实体,再不能引用其他实体,引用也不能改变指向
	//所以这里的操作是赋值而不是引用
	int m = 20;
	k = m;

	return 0;
}

引用的使用:

  • 引用在实践中主要是于引用传参和引用做返回值中减少拷贝提高效率改变引用对象时同时改变被引用对象
  • 引用传参跟指针传参功能是类似的,引用传参相对更方便⼀些。
  • 引用返回值的场景相对比较复杂,我们在这里后续会简单讲一下场景,还有一些内容后续在类和对象的学习中会继续深入了解。
  • 引用和指针在实践中相辅相成,功能有重叠性,但是各有特点,互相不可替代。C++的引用跟其他语言的引用(如Java)是有很大的区别的,除了用法,最大的点,C++引用定义后不能改变指向,Java的引用可以改变指向。
  • ⼀些主要用C代码实现版本数据结构教材中,使用C++引用替代指针传参,目的是简化程序,避开复杂的指针,但是我们可能当时还没学过引用,导致一头雾水。

--上述的使用在后面大部分都会有举例,还是需要注意看注释

举例说明:(注意看注释)

例子1:(交换值,在顺序表中的应用)

cpp 复制代码
//指针,引用
//大部分情况下引用都可以替代指针,除了一些特殊情况,比如链表的树的节点的定义只能使用指针

#include<iostream>
using namespace std;

void swap(int* x, int* y)
{
	int tmp = *x;
	*x = *y;
	*y = tmp;
}

void swap(int& x, int& y)
{
	int tmp = x;
	x = y;
	y = tmp;
}

int main()
{
	//swap函数交换值
	int a = 1; int b = 7;
	cout << a << " " << b << '\n';
	swap(&a, &b);//用指针
	cout << a << " " << b << '\n';
	swap(a, b);//用引用
	cout << a << " " << b << '\n';


	return 0;
}

--这里在顺序表中的使用博主就直接放图了,只展示部分可以和指针对比看看,具体的可以自己去实现一下。

注意在实现的过程中原来使用的一般都是 psl-> ,现在应该使用 psl. 。

例子2:(交换指针,在链表中的使用)

cpp 复制代码
//替代二级指针使用
#include<iostream>
using namespace std;

void swap(int**x, int** y)
{
	int*tmp = *x;
	*x = *y;
	*y = tmp;
}

void swap(int*& x, int*& y)
{
	int*tmp = x;
	x = y;
	y = tmp;
}

int main()
{
	//swap函数交换指针
	int a = 1; int b = 7;
	int* pa = &a; int* pb = &b;
	cout <<*pa << " " <<*pb << '\n';
	swap(&pa, &pb);//用指针
	cout << *pa << " " << *pb << '\n';
	swap(pa, pb);//用引用
	cout << *pa << " " << *pb << '\n';


	return 0;
}

--我们再来看看在链表中的使用,还是省略化的展示

cpp 复制代码
//在链表中的使用
#include<iostream>
using namespace std;

typedef struct SListNode
{
	struct SListNode* next;
	int val;
}SLTNode;//, //*PSLTNode;

//typedef struct SListNode SLTNode;
//typedef struct SListNode* PSLTNode;


//以前使用二级指针的实现方法
//void SLTPushBack(SLTNode** pphead, int x)
//{
//	SLTNode* newnode; // = malloc
//
//	if (*pphead == NULL)
//	{
//		*pphead = newnode;
//	}
//	else
//	{
//		// 找到尾结点,newnode链接到尾结点
//	}
//}

//void SLTPushBack(PSLTNode& phead, int x)//这个在书上有时候会用
void SLTPushBack(SLTNode*& phead, int x)
{
	SLTNode* newnode = NULL; // = malloc,这里省掉过程

	if (phead == NULL)
	{
		phead = newnode;
	}
	else
	{
		// 找到尾结点,newnode链接到尾结点
	}
}


int main()
{
	//用二级指针
	SLTNode* plist = NULL;
	//SLTPushBack(&plist, 1);
	//SLTPushBack(&plist, 2);
	//SLTPushBack(&plist, 3);
	//SLTPushBack(&plist, 4);

	//用引用
	//PSLTNode plist = NULL;
	SLTNode* plist = NULL;
	SLTPushBack(plist, 1);
	SLTPushBack(plist, 2);
	SLTPushBack(plist, 3);
	SLTPushBack(plist, 4);

	return 0;
}

我们来看一个可能有的教材上会出现,但是很容易把我们看懵的形式:(注意看图片解释)


--在大部分的场景下,引用都可以代替指针使用,但是在链表,树的节点定义位置只能使用指针,引用无法替代。

这是因为:C++的引用无法改变指向,节点一定存在改变指向的场景

传值返回,传引用返回:

传值返回:(主要看错误的地方)

cpp 复制代码
#include<iostream>
using namespace std;
int fun()
{
	int ret = 0;

	return ret;
}

int main()
{
	int x = fun();//x接受的其实是ret的拷贝值,在fun函数销毁时ret就没了,通过临时变量带出
	//fun() += 1;//所以这里就无法直接修改
	return 0;
}

这里返回接收的是ret吗?--不是,是ret的临时拷贝

传引用返回:(主要看错误的地方)

cpp 复制代码
//传引用返回
#include<iostream>
using namespace std;

int& fun()
{
	int ret = 0;

	return ret;
}

int main()
{
	int&x = fun();
	cout << x << '\n';//这里可能会是随机值

	return 0;
}

这里的x可能是0也可能是随机值,为什么?

相当于野指针的访问,比较危险

--我们再来看看一个更奇怪的例子,为啥x后面没有变,但却是y的值,依旧是看错误的地方

cpp 复制代码
#include<iostream>
using namespace std;
int& func1()
{
	int ret = 0;
	// ...
	return ret;
}

int& func2()
{
	int y = 456;
	// ...
	return y;
}

int main()
{
	int& x = func1();
	cout << x << endl;
	func2();//明明没有修改x,但x的值还是会为y,这是因为fun1函数栈帧销毁后还给了操作系统,但是func2又使用了这块空间
	cout << x << endl;

	return 0;
}

为什么会出现这样的现象呢,结合上一个例子可以思考一下:

两个小问题:

1.销毁了之后为什么还可以访问?

--因为销毁了并不代表空间没了,内存空间是可以反复使用的,销毁的本质是还给操作系统,跟租房一样,返回的别名就相当于偷偷留了把钥匙,但是这样是不可取的。

2.这里相当于野指针的访问为什么没报错?

--越界不一定报错,越界写可能会报错,在数组中存在越界抽查这个现象。所以我们有时候编译器不会在这里报错的

传引用返回的正确使用:(顺序表为例)

--我们这里的实现还是简略的写一下,中间使用了引用的写法,大家刚好可以看看,注意看注释

SeqList.h:

cpp 复制代码
#pragma once
#include<stdio.h>
#include<stdlib.h>

typedef struct SeqList
{
	int* arr;
	int size;
	int capacity;

}SL;

void SLInit(SL& pls, int n = 4);
void SLPushBack(SL& pls, int x);
int SLFind(SL& pls, int x, int i = 0);
int& SLat(SL& pls, int i);//这里使用引用就又可以读取i位置的值,又可以修改了
void SLModify(SL& pls, int i, int x);//这个并不好用

SeqList.cpp:

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS 1
#include"SeqList.h"

void SLInit(SL& pls,int n)
{
	pls.arr = (int*)malloc(n * sizeof(int));
	pls.size = 0;
	pls.capacity = n;
}

void SLPushBack(SL& pls, int x)
{
	//..................
	pls.arr[pls.size++] = x;
}

int SLFind(SL& pls, int x,int i)
{
	while (i < pls.size)
	{
		//..................
	}

	return -1;
}

int& SLat(SL& pls, int i)//这里使用引用就又可以读取i位置的值,又可以修改了
{
	//......
	return pls.arr[i];//这里可以使用引用返回是因为它是结构体中的一个在堆的数据
}
	
void SLModify(SL& pls, int i, int x)//这个并不好用
{
	//......
	pls.arr[i] = x;
}

test.c:

cpp 复制代码
//那我们什么时候可以使用到这个传引用返回呢
//我们拿顺序表这个数据结构为例子,当然栈也可以

#include"SeqList.h"
#include<iostream>
using namespace std;

int main()
{
	SL s;
	SLInit(s, 10);
	for (size_t i = 0; i < 10; i++)
	{
		SLPushBack(s, i);
	}

	for (size_t i = 0; i < 10; i++)
	{
		cout << SLat(s, i) << " ";
	}
	cout << endl;

	// 把顺序表第i个位置的值修改为x
	int i = 0;
	int x = 0;
	cin >> i;
	cin >> x;

	//SLModify(&s, i, x);//不好用
	SLat(s, i) = x;//用了返回引用这里就可以直接修改赋值了

	for (size_t i = 0; i < 10; i++)
	{
		cout << SLat(s, i) << " ";
	}
	cout << endl;

	return 0;
}

--这里成功使用了传引用返回,所以是又可以读又可以写的

--补充一个传引用返回使用的情况就是使用static,大家可以看看

小知识点补充:

--语法是表达层(可以想一下老婆饼,夫妻肺片),底层是不一定一样的(就像老婆饼里没有老婆)

cpp 复制代码
#include<iostream>
using namespace std;

int main()
{
	int i = 0;
	//语法层面上引用不开空间,指针开空间
	int& j = i;
	int* p = &i;

	j++;
	(*p)++;

	//但是我们转到反汇编,可以看出其实引用的底层就是指针,两个指令的反汇编代码都差不多,这个需要仔细观察一下

	return 0;
}

--语法层面上引用是不开空间的,指针开空间,但是底层来看两者是差不多的的。

--引用还有部分知识点,会在下一篇博客中继续分享给大家

完整源代码:

CPP专属仓库: 【CPP知识学习仓库】 - Gitee.com


往期回顾:

【数据结构初阶】--排序(一):直接插入排序,希尔排序

【数据结构初阶】--排序(二)--直接选择排序,堆排序

【数据结构初阶】--排序(三):冒泡排序,快速排序

【数据结构初阶】--文件归并排序

结语:本篇博客就到此结束了,主要给大家分享了C++的输入输出,参数缺省,重载以及引用的知识点,但是引用的知识点还有一些,博主会在下一篇博客中继续给大家分享,如果文章对你有帮助的话,欢迎评论,点赞,收藏加关注,感谢大家的支持。