Bootstrap

【从零开始学习C++】(1)C++ 入门

什么是c++

C语言是结构化和模块化的语言,适合处理较小规模的程序。对于复杂的问题,规模较大的程序,需要高度
的抽象和建模时,C语言则不合适。为了解决软件危机, 20世纪80年代, 计算机界提出了OOP(object
oriented programming:面向对象)思想,支持面向对象的程序设计语言应运而生。
1982年,Bjarne Stroustrup博士在C语言的基础上引入并扩充了面向对象的概念,发明了一种新的程序语
言。为了表达该语言与C语言的渊源关系,命名为C++。因此:C++是基于C语言而产生的,它既可以进行C语
言的过程化程序设计,又可以进行以抽象数据类型为特点的基于对象的程序设计,还可以进行面向对象的程
序设计。

一、c++关键字

在这里我们可以先了解一下
在这里插入图片描述

二、c++的命名空间

或许很多人都知道在编写一个cpp程序时要加入 using namespace std ;
但你知道这究竟是为什么嘛?
———在C/C++中,变量、函数和后面要学到的类都是大量存在的,这些变量、函数和类的名称将都存在于全局作用域中,可能会导致很多冲突。使用命名空间的目的是对标识符的名称进行本地化,以避免命名冲突或名字污染,namespace关键字的出现就是针对这种问题的。

//命名空间的定义
//1. 定义命名空间首先用关键字namespace,在关键字后面加上名称,再加一对{}即可
namespace n1  //n1 为命名空间的名称
{
	int a = 0;  //命名空间中可以定义函数,也可定以变量
	int add(int a, int b)
	{
		return a + b;
	}
}

//2. 命名空间可以嵌套
namespace n2
{
	namespace n3
	{
		
	}
}

//3. 同一个工程中允许存在多个相同名称的命名空间,编译器最后会合成同一个命名空间中。
namespace n1
{
	int Mul(int left, int right)
	{
		return left * right;
	}
}

注意:一个命名空间定义了一个新的作用域,命名空间中的所有内容都局限于该命名空间当中

命名空间的使用

#include <iostream>
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;
	}
}
//定义了如上的空间N1 ,使用它一共有三种方式

int main()
{
	//printf("%d", a);//直接使用编译器会报错
}

//1.0 加命名空间名称及作用域限定符
int main()
{
	printf("%d ", N::a);
	printf("%d ", N::b);
}

//2.0 使用using将命名空间中成员引入
using N::b;
int main()
{
	printf("%d ", N::a);
	printf("%d ", b);
}

// 3.0  使用using namespace 命名空间名称引入
using namespace N;
int main()
{
	printf("%d ", a);
	printf("%d ", b);
	int c=add(1, 2);
	printf("%d ", c);
	int d=sub(4, 3);
	printf("%d ", d);
}

c++中的输入和输出

//c++中如何实现hello world
#include <iostream>
using namespace std;
int main()
{
	cout << "hello,world" << endl;
	return 0;
}

相比c语言来说我们发现c++的输出方式更为简单
说明:

  1. 使用cout标准输出(控制台)和cin标准输入(键盘)时,必须包含< iostream >头文件以及std标准命名空
    间。
    注意:早期标准库将所有功能在全局域中实现,声明在.h后缀的头文件中,使用时只需包含对应头文件
    即可,后来将其实现在std命名空间下,为了和C头文件区分,也为了正确使用命名空间,规定C++头文
    件不带.h;旧编译器(vc 6.0)中还支持<iostream.h>格式,后续编译器已不支持,因此推荐使用
    +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 << "  " << b << " " << c << endl;
	return 0;
}

运行结果如下
在这里插入图片描述

想必到达这里就会明白为什么要用using namespace std;这一串话了(这段话告诉编译器我们要用名字空间std中的函数或者对象)
但是日后当我们在写成熟的代码的时候,一般不建议将标准命名空间全部打开,而是需要用库里的什么就打开什么。这就有效的防止了命名冲突

例如写一个hello world
第一种就是上面提到的
不安全但是简单

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

第二种,用哪个,提前打开那个对象
安全,推荐使用

#include <iostream>
using std::cout;
using std::endl;
int mian()
{
	cout << "hello world" << endl;
}

第三种,什么时候用什么时候打开std
安全,但是较为复杂

#include <iostream>
int main()
{
	std::cout << "hello world" << std::endl;
}

三、 缺省参数

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

缺省参数又分为全缺省与半缺省

1.0全缺省

#include<iostream>
using namespace std;
void fun (int a = 10, int b = 20, int c = 30)
{
	cout << "a= " << a << endl;
	cout << "b= " << b << endl;
	cout << "c= " << c << endl<<endl;
}
int main()
{
	fun();// 不传参就用默认的
	fun(1,2,3);  //传了就用传了的
	fun(1);
	fun(1, 2);
}
运行结果如下

在这里插入图片描述
2.0 半缺省参数

#include<iostream>
using namespace std;
void fun2 (int a , int b , int c=30 )
{
	cout << "a= " << a << endl;
	cout << "b= " << b << endl;
	cout << "c= " << c << endl<<endl;
}
int main()
{
	fun2(1,2);  // 没传就用默认值
	fun2(1, 2, 3);

}

在这里插入图片描述

但要注意:

  1. 半缺省参数必须从右往左依次来给出,不能间隔着给
    eg:
    在这里插入图片描述

    在这里插入图片描述
    这三种写法都是错误的
  2. 缺省参数不能在函数声明和定义中同时出现
    eg:
//a.h
void TestFunc(int a = 10);
// a.c
void TestFunc(int a = 20)
{}
// 注意:如果生命与定义位置同时出现,恰巧两个位置提供的值不同,那编译器就无法确定到底该用那
个缺省值

3.缺省值必须是常量或者全局变量
4.C语言不支持(编译器不支持)

缺省函数的作用就是为了是函数调用更加灵活

四、函数重载

函数重载的概念::是函数的一种特殊情况,C++允许在同一作用域中声明几个功能类似的同名函数,这些同名函数的

形参列表(参数个数 或 类型 或 顺序)必须不同,常用来处理实现功能类似数据类型不同的问题

#include<iostream>
using namespace std;
int Add(int left, int right)
{	
	return left + right;
}
char Add(char left,char right)
{
	return left + right;
}
double Add(double left, double right)
{
	return left + right;
}
long Add(long left, long right)
{
	return left + right;
}
int main()
{
	cout<<Add(10, 20)<<endl;
	cout<<Add(10.0, 20.0)<<endl;
	cout<<Add(10L, 20L)<<endl;
	cout<<Add('1', '2')<<endl;
	return 0;
}

结果如下
在这里插入图片描述

注意:这两个函数就不构成运算符重载,因为参数的类型,个数都一样

short Add(short left, short right) 
{
 return left+right;
}
int Add(short left, short right)
 {
 return left+right;
 }

下面两个函数构成重载吗?

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

答案:不构成,他俩类型相同,只是缺省参数不同`

学习了函数重载,我们不禁联想到为什么c++支持函数重载,而C语言不支持函数重载呢?
在这里仅简单提一下,简单来说是因为C语言没办法支持重载,因为同名函数没办法区分。而C++是通过函数修饰规则来区分,只要参数不同,修饰出来的名字就不一样,就支持了重载。

**

extern “C”

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

extern "C" int Add(int left, int right);
int main()
{
Add(1,2);
return 0;
}

五、引用

引用的概念:引用不是定义一个新的变量,而是给已知的变量取一个别名,编译器不会为引用变量开辟内存空间,它和它引用的的变量共用同一块内存空间。 (通俗来说,引用就是取别名)
就比如:比如:李逵,在家称为"铁牛",江湖上人称"黑旋风”。

int main()
{

	int a = 10;

	//&在类型后面,就是引用的意思
	int& b = a;// b就是a的引用(别名)
	b = 20;

	int& c = b;//c就是b的引用
	c = 30;

}

在这里插入图片描述
通过监视我们可以看出完全符合引用的概念。

引用的作用
1.0 我们最熟悉的也就是交换两个值

void swap(int* a, int* b) //C语言中的交换需要指针(形参的改变不影响实参的改变)
{
	int tmp = *a;
	*a = *b;
	*b = tmp;
}

void swap(int& a, int& b)
{
	int r = a;
	a = b;
	b = r;

}
int main()
{
	int a = 10;
	int b = 20;
	swap(a,b);
	swap(&a, &b);
}

  • 引用特性
  1. 引用在定义时必须初始化
  2. 一个变量可以有多个引用
  3. 引用一旦引用一个实体,再不能引用其他实体
int main()
{
	//一个变量可以有多个别名
	int a = 0;
	int& b = a;
	int& c = b;
	int& d = c;

    int &x=a;
	int r = 10;
	x = r;  //将r的值赋给x;
}

在这里插入图片描述

常引用
eg1:

int main()
{
	const int a = 10;//const修饰代表a不能修改
	//int& ra = a;// 错误 ra引用a属于权限的放大,所以不行
	const int& ra = a;//正确

	int b = 10;
	int& rb = b;
	const int& crb = b;//正确,crb引用b属于权限的缩小,所以可以
   //b是可读可写的,crb是可读的,权限缩小,所以可行
}


eg2:
```cpp
int main()
{
	int c = 10;
	double d = 1.11;
	d = c; 
}

上述转换是可以的,发生了隐式类型转换,首先产生一个中间临时变量,这个中间临时变量是double类型的,将c赋给临时变量,再将临时变量赋给d

int main()
{ 
     int c=10;
    double& rc=c;  //错误写法,编译通不过
    const double &rc=c;//正确写法
}

后者正确是因为:发生类型赋值过程当中,将int给double,中间就会产生临时变量,临时变量是double类型,所以rc引用的是这个临时变量,并且这个临时变量具有常性(只是可读的),所以加了const后编译通过,类似于eg1中的第一个情况。

使用场景
1.0 做参数

void Swap(int& left, int& right) {
 int temp = left;
 left = right;
 right =templ;
 }

2.0 做返回值

在这里首先简单介绍下传值返回(见下图)
(1)传值返回时,调用Add完成后会产生int类型的临时变量,c给了临时变量,临时变量再给给ret
返回的对象是c的拷贝
在这里插入图片描述
通过上图我们可以证明确实产生了临时变量(ret引用的不是c,其实是这个临时变量,临时变量具有常性,所以第二个引用编过了)

(2)传引用返回
传引用返回 ,返回的返回对象是c的引用(别名)

int &Add(int a, int b)
{
	int c = a + b;
	return c;
}
int main()
{
	int ret = Add(1, 2);
	return 0;
}

在这里插入图片描述
主函数中并没有加const编译通过,所以证明返回的是c的引用

这段代码看着是没问题,编译器也通过了,但它实际上却存在这问题,ret的值是不确定的
解释如下,调用函数就会开辟栈帧

在这里插入图片描述
接下来再看这段代码‘

int &Add(int a, int b)
{
	int c = a + b;
	return c;
}
int main()
{
	int &ret = Add(1, 2);
	Add(5, 7);
	cout << ret << endl;
	return 0;
}

在这里插入图片描述
解释如下:
在这里插入图片描述

int &Add(int a, int b)
{
	int c = a + b;
	return c;
}
int main()
{
	int &ret = Add(1, 2);
	Add(5, 7);
	printf("hello\n");
	cout << ret << endl;
	return 0;
}

在这里插入图片描述
这里ret是随机值原理和上述还是一样,调用printf时c那块空间就是随机值

注意:一个函数调用就会向下空间建立一个栈帧,函数调用结束,栈帧就会销毁

实际过程中,出了函数作用域,返回对象就不存在了,不能用引用返回
加了static就可正常使用了

int &Add(int a, int b)
{
	static int c = a + b;
	return c;
}
int main()
{
	int &ret = Add(1, 2);
	Add(5, 7);
	printf("hello\n");
	cout << ret << endl;
	return 0;
}

在这里插入图片描述
ret的值也不会改变。

小小总结下:如果函数返回时,出了函数作用域,如果返回对象还未还给系统(也就是没销毁,例如加了static),则可以使用引用返回,如果已经还给系统了,则必须使用传值返回。

**

传参和传引用效率

当参数和返回值是比较大的变量时,传引用传参和传引用做返回值可以提高效率,只要符合条件,尽量用引用传参,传返回值

2.引用与指针的区别

int main()
{
	int a = 10;
	//在语法上,这里给a这块空间取了一块别名,没有开辟新空间
	int& ra = a;
	ra = 20;
	//在语法上,这里定义个指针变量,开辟了4个字节,存储a的地址
	int* pa = &a;
	*pa = 20;
}

在这里插入图片描述
但从汇编来看,引用的底层也是类似指针存地址的方式进行的

引用和指针的不同点:

  1. 引用在定义时必须初始化,指针没有要求
  2. 引用在初始化时引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何一个同类型实体
  3. 没有NULL引用,但有NULL指针
  4. 在sizeof中含义不同:引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32位平台下占4个字节)
  5. 引用自加即引用的实体增加1,指针自加即指针向后偏移一个类型的大小
  6. 有多级指针,但是没有多级引用
  7. 访问实体方式不同,指针需要显式解引用,引用编译器自己处理
  8. 引用比指针使用起来相对更安全

六、内联函数

c++频繁调用的函数,定义成inline,会在调用的地方展开,没有栈帧的开销


inline int add(int x,int y)  //直接在函数前加一个inline构成inline函数
{
	return x + y;
}
int main()
{
	int c = 0;

}

c语言为了小函数避免建立栈帧的消耗-》》提供了宏函数支持,预处理阶段展开
既然c语言已经解决了,为什么c++还要提供inline函数呢?(宏函数的缺点)
a.不支持调试 b.宏函数语法复杂容易出错 c.没有类型安全的检查

例如写一个add的宏函数

#define Add(int x,int y) return x+y;//典型的错误写法
#define Add(x,y) x+y; //错误 分号问题
#define Add(x,y) (x)+(y) // 错误 优先级问题

//标准写法
#define Add(x,y) ((x)+(y))

内联函数特性

  1. inline是一种以空间换时间的做法,省去调用函数额开销。所以代码很长或者有循环/递归的函数不适宜使用作为内联函数。
  2. inline对于编译器而言只是一个建议,编译器会自动优化,如果定义为inline的函数体内有循环/递归等等,编译器优化时会忽略掉内联。
  3. inline不建议声明和定义分离,分离会导致链接错误。因为inline被展开,就没有函数地址了,链接就会找不到。

七、auto关键字

    int a = 10;
	auto b = a;//类型声明成auto,可以根据a的类型自动推到b的类型
int main()
{
	int x = 10;
	auto a = &x;
	auto* b = &x;
	int& y = x;
	auto c = y;
	auto& d = x; //指定了d是x的引用

	//打印变量的类型
	cout << typeid(x).name() << endl;
	cout << typeid(a).name() << endl;
	cout << typeid(b).name() << endl;
	cout << typeid(y).name() << endl;
	cout << typeid(c).name() << endl;
}

在这里插入图片描述
2.0 在同一行定义多个变量
当在同一行声明多个变量时,这些变量必须是相同的类型,否则编译器将会报错,因为编译器实际只对第一个类型进行推导,然后用推导出来的类型定义其他变量。

void TestAuto()
{
 auto a = 1, b = 2; 
 auto c = 3, d = 4.0; // 该行代码会编译失败,因为c和d的初始化表达式类型不同
}

auto不能推到的场景
1.auto不能作为参数的函数

// 此处代码编译失败,auto不能作为形参类型,因为编译器无法对a的实际类型进行推导
void TestAuto(auto a)
{}

2.auto不能用来推导数组

void TestAuto()
{
 int a[] = {1,2,3};
 auto b[] ={4,5,6}}

八、 基于范围的for循环

对于一个有范围的集合而言,由程序员来说明循环的范围是多余的,有时候还会容易犯错误。因此C++11中引入了基于范围的for循环。for循环后的括号由冒号“ :”分为两部分:第一部分是范围内用于迭代的变量,第二部分则表示被迭代的范围。

int main()
{
	int arr[] = { 1,2,3,4,5 };
	for (int i = 0;i < sizeof(arr) / sizeof(arr[0]);i++)  //常规方法
	{
		cout << arr[i] << " ";
	}
	cout << endl;
	//范围for c++11 新语法遍历,更简单
	//自动遍历,依次取出arr中的元素,赋值给e,直至结束(e可以任意取其他名称)
	for (auto e : arr)
	{
		cout << e << " ";
	}
	cout << endl;
	return 0;

}

但如果我们想要改变数组的内容我们就需要借助引用
见如下代码

#include<iostream>
using namespace std;
int main()
{
	int arr[] = { 1,2,3,4,5,6 };
	//自动遍历,依次取出arr中的元素,赋值给e,直至结束
	for (auto e : arr)
	{
		e *= 2;
	}
	for (auto ee : arr)
	{
		cout << ee<<"  ";
	} 
	return 0;
}

在这里插入图片描述
是不是以为会输出2 4 6 8 10 12,但这个范围for 是自动遍历,依次取出arr中的元素,赋值给e,直至结束,arr是不会改变的如果想要改变增加引用即可

#include<iostream>
using namespace std;
int main()
{
	int arr[] = { 1,2,3,4,5,6 };
	//自动遍历,依次取出arr中的元素,赋值给e,直至结束
	for (auto& e : arr)
	{
		e *= 2;
	}
	for (auto ee : arr)
	{
		cout << ee<<"  ";
	} 
	return 0;
}

在这里插入图片描述
这样就达到了我们的需求,e是arr每个成员的别名,e改变arr成员也改变。

九、指针空值nullptr(C++11)

NULL实际是一个宏,在传统的C头文件(stdio.h)中,可以看到如下代码:

#ifndef NULL
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif
#endif

可以看到,NULL可能被定义为字面常量0,或者被定义为无类型指针(void*)的常量。不论采取何种定义,在使用空值的指针时,都不可避免的会遇到一些麻烦,比如:

void f(int a)
{
cout<<"f(int)"<<endl;
}
void f(int*a)
{
cout<<"f(int*)"<<endl;
}
int main()
{
f(0);
f(NULL);
f(nullptr);
return 0;
}

在这里插入图片描述
程序本意是想通过f(NULL)调用指针版本的f(int*)函数,但是由于NULL被定义成0,因此与程序的初衷相悖,所以引入nullptr来解决这一问题。

**注意:

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