C++入门

前言:在我们学习完C语言之后,开始学习C++。在C++入门学习中,我们会了解到C语言的一些不足之处,和在C++中是如何来解决此类问题的

一.命名空间

在学习C语言的时候,我们都知道不能使用关键字作为变量名来使用。如果我们在不知情的情况下使用了可能会遇到下面这种情况。

#include <stdio.h>
#include <stdlib.h>

int rand = 0;

int main()
{
	printf("%d\n", rand);
	return 0;
}
//这里将报错
//因为我们定义的变量rand和头文件中包含的函数名rand重定义

因为在C/C++中的变量,函数和后面要学到的类都是大量存在的,这些变量、函数和类的名称将都存在于全局作用域中,可能会导致很多冲突。因此在C++中为了解决此类问题提出了命名空间的概念。用来避免命名冲突或命名污染问题

常见的命名冲突:

  • 包含头文件,可能和库里面发生冲突
  • 将自己写的代码和别人的代码进行合并的时候,可能会发生冲突

1.1域

在C语言中,就提到了作用域这一概念。

int a = 1; //全局域

int main()
{
	//局部域
	int a = 0;  

	//优先访问局部域,局部域中没有,在访问全局域
	printf("%d\n", a);

	//我们可以使用域操作符(::)两个冒号来指定访问那个域
	// ::前面是空白,就默认是全局域(前面有无空格不影响)
	printf("%d\n", ::a);
	return 0;
}

在这里插入图片描述

1.2命名空间的定义

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

int a = 1;

//命名空间域
namespace lxl
{
	int a = 2;
}

int main()
{
	int a = 0;

	printf("%d\n", a);
	printf("%d\n", ::a);
	
	//如果我们不指定访问命名空间域或者展开命名空间域
	//默认是不会去命名空间域中查找的
	return 0;
}
//1.命名空间中可以定义变量/函数/类型
namespace lxl
{
	int rand = 10;

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

	struct Node
	{
		struct Node* next;
		int val;
	};
}
//2. 命名空间可以嵌套
namespace N1
{
	int a;
	int b;
	int Add(int left, int right)
	{
		return left + right;
	}

	namespace N2
	{
		int c;
		int d;
		int Sub(int left, int right)
		{
			return left - right;
		}
	}
}

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

1.3命名空间的三种使用方式

1.3.1指定访问

  • 加命名空间名称及作用域限定符
namespace lxl
{
	int a = 0;
	int b = 1;
	int c = 2;
}

int main()
{
    //域操作符(::)两个冒号
	printf("%d\n", lxl::a);
	return 0;
}

1.3.2部分展开

  • 使用using将命名空间中某个成员引入
using lxl::b;
int main()
{
	printf("%d\n", b);

	return 0;
}

1.3.3 全部展开

  • 使用using来展开命名空间
using namespace lxl;
int main()
{
	printf("%d\n", a);
	printf("%d\n", b);
	printf("%d\n", c);

	return 0;
}

1.4展开命名空间的注意事项

命名空间全部展开之后,命名空间中的变量,函数,和类型等,将全部暴露在全局中。

  1. 可能会和库/和别人的代码发生冲突
  2. 如果展开后的命名空间中的变量和全局变量名一样,将会报错 a不明确(相当于有两个相同的全局变量)

1.5std命名空间的使用惯例

std是C++标准库的命名空间。那我们使用的时候有两种使用使用方法。一种是全部展开,一种是部分展开。两种展开的区别大家都已经了解,那什么场景中应该使用哪种方式?

  1. 在日常练习中,建议直接using namespace std即可,这样就很方便
  2. 在一些项目中,建议展开常用的库对象/类型等方式

1.6命名空间的合并

同一个工程中允许存在多个相同名称的命名空间,编译器最后会合并成同一个命名空间中。

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

注意:

  • 如果两个命名空间中的变量名相同,将会报错。

二. C++输入&输出

#include<iostream>
// std是C++标准库的命名空间名,C++将标准库的定义实现都放到这个命名空间中

//using namespace std;
using std::cout;
using std::endl;
using std::cin;

int main()
{
	int a = 0;
	double b = 0;
	char c = 0;
	char d[] = "hello world!!";

	cin >> a >> b >> c;
	cout << a << ' ' << b << ' ' << c << endl << d;
	return 0;
}

2.1cout & cin

cout就相当于C语言中的Printf,cin就相当于scanf。

  1. cout和cin是全局的流对象,endl是特殊的C++符号,表示换行输出,他们都包含在包含< iostream >头文件中。
  2. 使用cout标准输出对象(控制台)和cin标准输入对象(键盘)时,必须包含< iostream >头文件以及按命名空间使用方法使用std。

2.2<< & >>

<<是流插入运算符,>>是流提取运算符.

比较:

  • 使用printf/scanf时需要手动控制格式
  • C++的输入输出可以自动识别变量类型。

三.缺省参数

3.1概念

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

#include <iostream>
using namespace std;

int Add(int x = 3, int y = 2)
{
	return x + y;
}

int main()
{
	int sum = Add(); //没有传参使用默认的缺省值

	int sum2 = Add(10, 20); // 传参时,使用指定的实参

	printf("%d\n", sum);
	printf("%d\n", sum2);
	return 0;
}

在这里插入图片描述

3.2缺省参数分类

  • 全缺省:函数的每一个参数都指定一个缺省值
int Add(int x = 3, int y = 2)
{
	return x + y;
}
  • 半缺省:函数的参数没有全部被指定一个缺省值。
int Add(int x , int y = 2)
{
	return x + y;
}

int main()
{
	int sum = Add(3); 

	return 0;
}

3.3注意事项

指定缺省值时,我们应该注意,声明和定义不能同时给缺省值(会被认为重定义)。

  1. 缺省参数不能在函数声明和定义中同时出现,建议声明给缺省值,定义不给缺省值。
  2. 半缺省参数必须从右往左依次来给出,不能间隔着给。
  3. 缺省值必须是常量或者全局变量

3.4缺省参数解决的问题

在C语言中,我们不能像C++一样可以设置缺省值。因此在一些地方用起来不方便。列如:我们现在写一个顺序表,当数组的空间不够的时候我们可以扩容。但是当使用的人不给出数组的空间的时候,我们最开始给这个数组多大空间呢?多了浪费,少了需要扩容。因此,我们就可以设置一个缺省值。

四.函数重载

4.1概念

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

注意:如果只有返回值不同,不会构成函数重载。

int Add(int a = 3, int b = 4)
{
	return a + b;
}

double Add(double a = 3.0, double b = 4.0)
{
	return a + b;
}

int main()
{
	int sum = Add(2, 3);
	double dsum = Add(1.1, 2.2);

	cout << sum << endl;
	cout << dsum << endl;
	
	return 0;
}

在这里插入图片描述

这里大家是否有疑惑,为什么编译器可以根据形参列表的不同,而去调用对应的函数?
这是因为:函数重载可以自动识别参数的类型。 关于为什么可以自动识别参数类型,就必须了解C++中对函数名的修饰。

五.函数名修饰

在不同的平台,函数名的修饰规则不同。这里用Linux g++的命名规则举列子。

//Linux:_Z3Addii
// 3: 函数名的长度
// Add: 函数名
// ii:  形参的类型
int Add(int a = 3, int b = 4)
{
	return a + b;
}

而在C语言中,是直接用函数名,因此不能定义两个函数名相同的函数。

六.引用

6.1概念

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

类型& 引用变量名(对象名) = 引用实体

int main()
{
	int a = 4;
	//给a取别名
	int& b = a; 
	int& c = a;

	//给a的别名取别名
	int& d = b;
	int& e = d;

	cout << &a << endl;
	cout << &b << endl;
	cout << &c << endl;
	cout << &d << endl;
	cout << &e << endl;

	return 0;
}

在这里插入图片描述

注意:

  1. 可以给实体取多个别名,也可以给别名取别名。
  2. 引用类型必须和引用实体是同种类型的。
  3. 引用在定义时必须初始化(有实体)
  4. 引用一旦引用一个实体,再不能引用其他实体。

6.2引用的使用场景

6.2.1做输出型参数,提高效率

输出型参数:形参的改变会影响实参。

  • C语言
void exchange1(int* x, int* y)
{
	int tmp = *x;
	*x = *y;
	*y = tmp;
}

int main()
{
	int a = 3;
	int b = 5;

	exchange1(&a, &b);
	printf("%d\n%d\n", a, b);

	return 0;
}
  • 引用
void exchange(int& x, int& y)
{
	int tmp = x;
	x = y;
	y = tmp;
}

int main()
{
	int a = 3;
	int b = 5;

	exchange(a, b);
	cout << a << endl;
	cout << b << endl;

	return 0;
}
  • 结构体
typedef struct ListNode
{
	int val;
	struct ListNode* next;
}*PNode;

void LTPushBack(PNode& phead, int x);
  • 引用做参数提高效率主要体现在大对象或深拷贝类对象的时候。

6.2.2引用做返回值

  • 1.直接返回值, 会生成临时变量。
int Add(int x, int y)
{
	int c = x + y;

	return c;
}

int main()
{
	int a = 3;
	int b = 2;

	int sum = Add(a, b);
	cout << sum << endl;
	return 0;
}

传值调用返回的时候因为函数栈帧的销毁,此时需要一个临时变量(可能是寄存器) 来拷贝C的值,最后在将临时变量的值拷贝给sum。一共需要拷贝两次。

  • 2.引用做返回值,不会生成临时变量,这样可以减少拷贝次数,提高效率。
int& Func()
{
	static int n = 0;
	n++;

	return n;
}

int main()
{
	int ret = Func();
	cout << ret << endl;
	return 0;
}

此时引用做返回值,返回n的别名,因此返回的时候不会创建临时变量,在将n拷贝给ret。(拷贝一次)

  • 3.获取和修改返回值
struct SeqList
{
	int a[100];
	size_t size;
};

int SLGet(SeqList* ps, int pos)
{
	assert(pos < 100 && pos >= 0);

	return ps->a[pos];
}

void SLModify(SeqList* ps, int pos, int x)
{
	assert(pos < 100 && pos >= 0);

	ps->a[pos] = x;
}

int& SLAt(SeqList& s, int pos)
{
	assert(pos < 100 && pos >= 0);

	return s.a[pos];
}

int main()
{
	SeqList s;
	//C方式
	SLModify(&s, 0, 1);
	cout << SLGet(&s, 0) << endl;
	// 对第0个位的值+5
	int ret1 = SLGet(&s, 0);
	SLModify(&s, 0, ret1+5);
	cout << SLGet(&s, 0) << endl;

	//引用返回获取和修改返回值
	SLAt(s, 0) = 1;
	cout << SLAt(s, 0) << endl;
	SLAt(s, 0) += 5;
	cout << SLAt(s, 0) << endl;
	return 0;
}

6.3引用做返回值的注意事项

  • 不能返回局部变量

  • 错误案例

int& Func(int x)
{
	int n = x;
	n++;

	return n;
}

int main()
{
	int ret1 = Func(10); 
	cout << ret1 << endl;
	//函数栈帧销毁,此时返回n的别名,在赋值给ret1
	//此时n的值是不确定的

	//验证:
	//函数栈帧销毁,此时返回n的别名,在给n的别名取别名ret2
	//此时再次调用打印函数,之前的Func的函数空间被打印函数使用
	//里面的数据发生变化,因此n的值变为随机数
	int& ret2 = Func(20);
	printf("xxxxxxx\n");
	cout << ret2 << endl;
	return 0;
}

在这里插入图片描述

  • 正确的案例:
int& Func(int x)
{
	static int n = x;
	n++;

	return n;
}

int main()
{
	int& ret = Func(10);
	printf("xxxxxxxx\n");
	cout << ret << endl;
	return 0;
}

在这里插入图片描述

因此使用引用做返回值得场景是:出了作用域之后返回的对象没有还给操作系统(static,malloc, 结构体…)。如果出了作用域之后返回值还给操作系统就只能使用传值返回。

6.4常引用

  • 传值返回会创建临时变量,同时临时变量具有常属性。

在这里插入图片描述

正确做法:

int Func(int x)
{
	static int n = x;
	n++;

	return n;
}

int main()
{
	const int& ret = Func(10);
	return 0;
}
  • 发生类型转换的时候,也会产生临时变量。 相同类型不会。

在这里插入图片描述

正确做法:

int main()
{
	double d = 3.14;
	const int& a = d;

	cout << a; //3
	return 0;
}

为什么发生类型转换的时候会产生临时变量呢?

列如:我们需要将double类型的数据转换为int类型,此时产生一个临时变量,将double的值拷贝后,转换为int类型在赋值给int变量(如果没有临时变量,此时原double的值就会被改变)

6.5权限

void TestConstRef()
{
	const int a = 10;
	//int& ra = a;   // 权限放大, a为常量
	const int& ra = a; // 权限平移

	int b = 10;
	// int& rb = 10; // 权限放大,10为常量
	const int& rb = 10; //权限平移

	int c = 10;
	const int& rc = c; //缩小rc作为别名的权限
	c++; //c可改,rc不可改

	double d = 12.34;
	//int& rd = d; // 类型转换,创建临时变量,具有常属性
	const int& rd = d; //权限平移

	
}

注意:权限可以缩小/平移,但是不能放大。

6.6引用和指针的区别

  • 语法概念上引用就是一个别名,没有独立空间,和其引用实体共用同一块空间。
  • 底层实现上实际是有空间的,因为引用是按照指针方式来实现的。

在学习C++前期,我们暂时认为引用是没有独立空间的就好了。

6.6.1引用和指针的不同点

  1. 引用概念上是变量的别名,指针则是存储变量的地址。
  2. 引用在定义的时候必须初始化,指针没有要求。
  3. 引用初始化之后就不能在引用别的实体,指针可以存储同类型的不同变量的地址。
  4. 没有NULL引用,但又NULL指针。
  5. 在sizeof中含义不同:引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(4/8)
  6. 引用加一则实体加一,指针加一则向后偏移一个指针类型的大小。
  7. 没有多级引用,有多级指针。
  8. 访问实体方式不同,指针需要显式解引用,引用编译器自己处理
  9. 引用比指针使用起来相对更安全

七.内联函数

7.1概念

inline修饰的函数叫做内联函数编译时C++编译器会在调用内联函数的地方展开,没有函数调用建立栈帧的开销,内联函数提升程序运行的效率。

7.2内联函数和函数的区别

  • 函数

在这里插入图片描述

当每调用此函数时,都会去栈上开辟空间。如果调用频繁就会有建立栈帧的开销,降低程序的运行效率。

  • 内联函数

在这里插入图片描述

内联函数会在调用的地方展开,这样就能减少建立函数栈帧的开销,并且并且能提高程序的运行效率。

7.3内联函数的特性

  1. inline对于编译器而言只是一个建议,不同编译器关于inline实现机制可能不同。一般建议将函数规模小的,频繁调用的的函数采用inline修饰。对于规模较大的函数和递归编译器可能会忽略inline特性。
  2. inline是一种以空间换时间=的做法,如果编译器将函数当成内联函数处理,在编译阶段,会用函数体替换函数调用,缺陷:可能会使目标文件变大,优势:少了调用开销,提高程序运行效率
  • 列如:有10000个地方调用Func函数(50行代码)
    1. 如果直接函数调用将会产生10000(call指令) + 50 行指令。
    1. 如果内联展开将会产生10000 * 50行指令。
  1. inline不建议声明和定义分离,分离会导致链接错误。因为inline被展开,就没有函数地址了,链接就会找不到。

7.4C语言中使用宏来解决此类问题

#include <iostream>
using namespace std;
#define Add(x, y) ((x) + (y))

int main()
{
	int a = 2;
	int b = 3;

	cout << Add(a, b) << endl;

	return 0;
}

7.5宏的优缺点

  • 优点

    1. 增强代码的复用性。
    1. 提高性能。
  • 缺点

    1. 不方便调试宏。(因为预编译阶段进行了替换)
    1. 代码可读性差,可维护性差,容易误用。
    1. 没有类型安全的检查 。

7.6C++中如何替代宏

  1. 常量定义 换用const enum
  2. 短小函数定义 换用内联函数

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

8.1auto简介

C++11中,auto作为一个新的类型指示符来指示编译器,auto声明的变量必须由编译器在编译时期推导而得。通俗来说就是:auto可以根据右边的表达式,自动推导类型

int main()
{
	int a = 10;

	//根据右边的表达式,自动推导类型
	auto b = a;
	auto d = 1 + 1.1;

	//typeid 是打印类型的函数
	cout << typeid(b).name() << endl;
	cout << typeid(d).name() << endl;

	return 0;
}

在这里插入图片描述

注意使用auto定义变量时必须对其进行初始化因为在编译阶段编译器需要根据初始化表达式来推导auto的实际类型。(因此auto并非是一种“类型”的声明,而是一个类型声明时的“占位符”,编译器在编译期会将auto替换为变量实际的类型。)

8.2auto的使用场景

  1. auto一般在类型很长的时候使用,来进行长类型替换。
  2. 类型难于拼写
  3. 含义不明确导致容易出错
#include <string>
#include <map>
int main()
{
	std::map<std::string, std::string> m{ { "apple", "苹果" }, { "orange",
   "橙子" },
	  {"pear","梨"} };

	//std::map<std::string, std::string>::iterator 是一个类型

	std::map<std::string, std::string>::iterator it1 = m.begin();
	
	//使用auto来进行长类型的替换
	auto it2 = m.begin();

	return 0;
}

8.3auto使用的细则

8.3.1auto与指针和引用结合起来使用

  • 用auto声明指针类型时,用auto和auto*没有任何区别,但用auto声明引用类型时则必须加&
int main()
{
	int x = 10;

	//通过右边推出是指针
	auto a = &x;
	//指定右边必须是指针
	auto* b = &x;

	auto& c = x;

	cout << typeid(a).name() << endl;
	cout << typeid(b).name() << endl;
	cout << typeid(c).name() << endl;

	*a = 20;
	*b = 30;
	c = 40;

	return 0;
}

8.3.2同一行定义多个变量

  • 当在同一行声明多个变量时,这些变量必须是相同的类型,否则编译器将会报错,因为编译器实际只对第一个类型进行推导,然后用推导出来的类型定义其他变量。
int main()
{
	auto a = 1, b = 2;

	// 该行代码会编译失败,因为c和d的初始化表达式类型不同
	auto c = 3, d = 4.0;  
	
	return 0;
}

8.4auto不能推导的场景

    1. auto不能作为函数的参数
    1. auto不能直接用来声明数组
void TestAuto()
{
    int a[] = {1,2,3};
    //不能直接用来声明数组
    auto b[] = {456};
}

九.基于范围的for循环(C++11)

9.1 范围for的语法

for(类型 变量名 : 数组)

  • 在C++98中如果要遍历一个数组,可以按照以下方式进行:
void TestFor()
{
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循环后的括号由冒号“ :”分为两部分:第一部分是范围内用于迭代的变量,第二部分则表示被迭代的范围。
int main()
{
	int arr[] = { 1, 2, 3, 4 };

	for (auto e : arr)
		cout << e << ' ';

	return 0;
}

在这里插入图片描述


若我们给数组中的元素每个乘2,又该如何写呢?

  • 错误案列
int main()
{
	int arr[] = { 1, 2, 3, 4 };

	for (auto e : arr)
		e *= 2;

	for (auto e : arr)
		cout << e << ' ';

	cout << endl;

	return 0;
}

在这里插入图片描述

解释:因为此时的范围for是取数组中的内容,然后拷贝给e,e的改变并不会影响数组的内容。

  • 正确案列(引用)
int main()
{
	int arr[] = { 1, 2, 3, 4 };

	for (auto& e : arr)
		e *= 2;

	for (auto& e : arr)
		cout << e << ' ';

	cout << endl;
	return 0;
}

在这里插入图片描述

注意:与普通循环类似,可以用continue来结束本次循环,也可以用break来跳出整个循环。

9.2范围for的使用条件

    1. for循环迭代的范围必须是确定的,适用于数组。 对于类而言,应该提供begin和end的方法,begin和end就是for循环迭代的范围。

注意:以下代码就有问题,因为for的范围不确定

void TestFor(int array[])
{
    for(auto& e : array)
        cout<< e <<endl;
}

解释:此时这里的array是指针,不是数组。

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

  • 在C++98中NULL实际是一个宏,在传统的C头文件(stddef.h)中。
#ifndef NULL
#ifdef __cplusplus
#define NULL   0
#else
#define NULL   ((void *)0)
#endif
#endif

可以看到NULL可能被定义为字面常量0,或者被定义为无类型指针(void*)的常量,编译器默认情况下将其看成是一个整形常量,如果要将其按照指针方式来使用,必须对其进行强转(void *)0。不论采取何种定义,在使用空值的指针时,都不可避免的会遇到一些麻烦,比如

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

void f(int*)
{
	cout << "f(int*)" << endl;
}

int main()
{
	f(0);
	f(NULL);
	return 0;
}

在这里插入图片描述

程序本意是想通过f(NULL)调用指针版本的f(int*) 函数,但是由于NULL被定义成0,因此与程序的初衷相。

  • 在C++11中使用nullptr来表示空指针。
void f(int)
{
	cout << "f(int)" << endl;
}

void f(int*)
{
	cout << "f(int*)" << endl;
}

int main()
{
	f(0);
	f(NULL);
	f((int*)NULL);
	f(nullptr);
	return 0;
}

在这里插入图片描述

注意

    1. . 在使用nullptr表示指针空值时,不需要包含头文件,因为nullptr是C++11作为新关键字引入的。
    1. 在C++11中,sizeof(nullptr) 与 sizeof((void)0)所占的字节数相同。
    1. 为了提高代码的健壮性,在后续表示指针空值时建议最好使用nullptr。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

清隆綾小路

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值