C++入门

目录

一.命名空间

1.引入

2.正常命名空间定义

3.其它定义方法

4.命名空间的使用

1.加命名空间和作用限定符::

2.利用using将命名空间的某个成员引入

3.利用using namespace+命名空间名称将其直接展开

二.C++输入&输出

三.缺省参数

1.概念介绍

2.缺省参数分类

1.全缺省

2.半缺省

四.函数重载

1.概念介绍

2.原理——名字修饰

3.不同的函数重载类型

1.参数类型不同

2.参数个数不同

3.参数类型顺序不同

五.引用

1.概念介绍

2.用法

3.应用特性

1.引用必须初始化使用

2.一个变量,可能有多个引用

3.一旦引用了一个变量,就不能再引用其它变量

4.使用场景

1.输出型参数

2.返回值

5.指针和引用的区别 

六.内联函数

七.auto(C++11)

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

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


先来看看c++祖师爷的照片

本章的C++基础都是建立在C语言地基下,祖师爷对C语言觉得不合适的地方,进行的修改.

一.命名空间

1.引入

我们先看一段简单的程序

#include <stdio.h>
#include <stdlib.h>
int rand = 10;
//C语言没有办法解决类似的命名问题
int main()
{
	printf("%d\n",rand);

	return 0;
}

 运行后,程序会崩溃,然后出现上面的报错.

其实我们也很好理解,由于我们包含了stdlib.h这个头文件,在链接的时候,由于原先的库函数中

也有rand这个同名的函数,因此编译器无法识别rand是函数还是我们所希望的变量,因此程序发生

了崩溃.

这看上去是一个小的问题,但是在生活却很常见.

假如一份任务被划分给不同小组去完成不同的部分,在最后阶段,需要把所有人的代码合并起来,

这时候,我们就无法保证不同小组定义的变量名是否会一样(发生冲突).

但是C语言中有一种场景,允许我们变量名是相同,但不会发生报错.

#include <stdio.h>
#include <stdlib.h>
//域
//局部域/全局域:1.使用2.生命周期
void f1()
{
	int a = 0;
	//默认局部优先
	printf("f1函数的a:%d\n", a);
	printf("f1函数的a的地址:%p\n", &a);
}

int main()
{
	int a = 1;
	printf("main函数的a:%d\n", a);
	printf("main函数的a的地址:%p\n", &a);
	f1();
	return 0;
}

先看上面这段程序,可以得到如下结果.

    

我们注意到在函数f1中的局部变量a和main函数中的局部变量a地址并不相同,

也就是说它们并不是同一个变量,但它们是可以重名的,这是为什么呢?

这就涉及一个很重要的概念——域.

我们把f1函数看作是一个域,里面创建的变量a(局部变量),它只能在函数f1中出现,并且使

用,一旦f1函数调用结束,它就会被销毁.

同理,main函数中的变量a也是如此,它也是一个局部变量,它只能在main函数中出现,又由于

main函数在整个程序结束的时候,才会被销毁,所以main函数的变量a会一直存在,直到程序结

束.

就像一个个小的平行世界一样,即便它们拥有着相同的名字,它们一生的经历也并不相同(使用不

同,f1中的a被赋值为0,main函数中的a被赋值为1),生命长短也不同(生命周期不同).

可见,域影响了变量的使用,和变量的生命周期.

C++的命名空间也是如此,我们可以使用namespace关键字创建自己的命名空间.(域)

以此完成对标识符和名称进行本地化,以避免命名冲突或名字污染的目的

2.正常命名空间定义

使用方法也很简单namespace,后面跟命名空间的名字,然后接一对{}即可,{}中即为命

名空间的成员(可以是变量,也可以是函数)

比如我们之前链表和队列数据结构,都有Node这个结构体.

我们将其划分在不同的命名空间AQueue和BList中,就可以重名,而不需要在命名上下一番功夫进

行区分.

namespace AQueue 
{
	struct Node {
		struct Node* next;
		int val;
	};

	struct Queue {
		struct Node* head;
		struct Node* rear;
		int size;
	};
}

namespace BList {
	struct Node {
		struct Node* next;
		struct Node* prev;
	};
}

PS:虽然和域的概念很像,但严格意义来说,命名空间只能算是影响了变量的使用,两两变量处

在不同的命名空间下可以重名,但是生命周期并没有改变.

3.其它定义方法

除了普通定义外,命名空间还支持嵌套定义与合并.

//嵌套定义
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;
    }
  }
}

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

中.

假如还另外存在同样称为N1的命名空间,那编译器会将里面的内容,和另外N1的命名空间合并.

namespace N1
{
    int Mul(int left, int right)
    {
        return left * right;
    }
}

4.命名空间的使用

1.加命名空间和作用限定符::

//Queue.h
#pragma once

//命名空间 -- 命名空间域,只影响使用,不影响生命周期

namespace AQueue 
{
	struct Node {
		struct Node* next;
		int val;
	};

	struct Queue {
		struct Node* head;
		struct Node* rear;
		int size;
	};
}
//List.h
#pragma once

namespace List {
	struct Node {
		struct Node* next;
		struct Node* prev;
	};
}
#include "List.h"
#include "Queue.h"

int main()
{   
	//命名空间名字如果还重合,则还是会报错
	struct AQueue::Node node1;
	struct List::Node node2;
	return 0;
}

2.利用using将命名空间的某个成员引入

#include "List.h"
#include "Queue.h"

//2.局部展开,一般情况下,建议局部展开最常用的成员
using AQueue::Node;
int main()
{
	struct Node node1;
	struct List::Node node2;
	return 0;
}

3.利用using namespace+命名空间名称将其直接展开

#include "List.h"
#include "Queue.h"

//3.全局展开,一般情况,不建议全局展开
using namespace AQueue;
int main()
{
	struct Node node1;
	struct Queue q;
	return 0;
}

二.C++输入&输出

在C语言中我们有标准输入输出函数scanf和printf,而在C++中我们有cin标准输入cout标准输出.

在C语言中使用scanf和printf函数,需要包含头文件stdio.h.

而在C++中使用cin和cout,需要包含头文件iostream以及std标准命名空间

相较于scanf,printf函数而言,cin和cout一个最大的好处,是其可以自动识别类型.

#include <iostream>
using namespace std;
int main()
{   
	// << 流插入  >> 流输出
	// endl等价于'\n'
	int n = 0;
	cin >> n;
	double* a = (double*)malloc(sizeof(double) * n);
	if (a == NULL)
	{
		perror("malloc fail.\n");
		exit(-1);
	}
    //自动识别类型输入
	for (int i = 0; i < n; ++i)
	{
		cin >> a[i];
	}
    //自动识别类型输出
	for (int i = 0; i < n; ++i)
	{
		cout << a[i] << " ";
	}
	cout << endl;

	return 0;
}

三.缺省参数

1.概念介绍

解决完C语言命名重叠问题后,C++祖师爷又将目光投向了函数参数设计上.

在栈的设计一节中,我们默认初始创建栈的空间为4个元素,不够再继续扩容.

但假如我们一开始就知道需要创建空间的大小呢?如果再不断进行扩容,会导致程序的效率底下,

于是,C++祖师爷设计了缺省参数的概念.

简单而言,就是在设计函数的时候,我可以指定默认初始值,如果用户提供自己的默认初始值,那

就按照用户提供的进行调用函数,否则,就按照我们的进行调用函数.

#include <iostream>
using namespace std;
struct Stack {
	int* a;
	int top = 0;
	int capacity = 4;
};
void StackInit(struct Stack* ps,int defaultCapacity = 4)
{
	ps->a = (int* )malloc(sizeof(int)*defaultCapacity);
	ps->top = 0;
	ps->capacity = defaultCapacity;
}
//缺省参数,使用缺省值,必须从右往左缺省
int main()
{
	Stack st1;
    //按1000进行初始化
	StackInit(&st1, 1000);
	Stack st2;
    //用户没有提供,按照设计的4进行初始化
	StackInit(&st2);
	return 0;
}

2.缺省参数分类

1.全缺省

顾名思义,全缺省也就是,所有函数形参都有默认初始值

void Print(int a = 10, int b = 20, int c = 30)
{
	cout << a << endl;
	cout << b << endl;
	cout << c << endl;
}

2.半缺省

只有部分函数形参具有默认初始值(比如下面代码中的a就没有默认初始值)

void Print(int a, int b = 20, int c = 30)
{
	cout << a << endl;
	cout << b << endl;
	cout << c << endl;
}

小细节:

1.半缺省参数只能从右往左依次给出

像下面的代码,c没有默认初始值,即便调用Print函数时,已经提供三个参数,程序依旧会发生报

错.

#include <iostream>
using namespace std;
//错误示例,没有从右往左提供缺省值
void Print(int a = 10, int b = 20, int c)
{
	cout << a << endl;
	cout << b << endl;
	cout << c << endl;
}
int main()
{
	Print(1,2,3);
	return 0;
}

2.缺省参数不能在函数声明和定义重复出现

假如定义和声明提供的缺省值不同,则编译器无法识别按照哪个缺省值作为默认初始化.

为了避免这种情况,缺省参数只能在声明或者定义中出现.

3.缺省值必须为常量或者全局变量

//正确示例
int x = 30;//全局变量
void Print(int a, int b = 20, int c = x)
{
	cout << a << endl;
	cout << b << endl;
	cout << c << endl;
}

四.函数重载

1.概念介绍

在设计函数名字上,祖师爷也有自己的想法.

比如我们设计一个加法函数,由于C语言的类型不同,我们需要设计很多个不同名字的函数,就为

了实现不同类型的加法操作,但这些函数实际上结构非常相似,仅仅是类型不同.

//整型加法
int Add_int(int x, int y)
{
	return x + y;
}
//浮点数加法
double Add_double(double x, double y)
{
	return x + y;
}
...

于是祖师爷就引入了函数重载的概念,简单点讲,上面的加法函数都可以用同一个名字Add,在调

用的时候,编译器会自动识别类型,找到对应的正确Add函数.

#include <iostream>
using namespace std;
int Add(int x, int y)
{
	return x + y;
}

double Add(double x, double y)
{
	return x + y;
}
int main()
{
	cout << Add(1, 2) << endl;    //打印1+2的结果
	cout << Add(1.2, 2.3) << endl;//打印1.2+2.3的结果
	return 0;
}

2.原理——名字修饰

在程序环境和预处理一节中,我们提到过C语言源文件的相应的处理方法,这里简单复习一下.

对于每一个.c源文件,都会经过预处理,编译,汇编三大过程,形成对应的目标文件(.obj)

在汇编阶段,每个源文件都会形成相应的符号表,符号表里面出现的符号,就是我们的函数名,全

局变量名之类的,并给我们每个对应的符号分配相应的内存地址.

在最后链接的阶段,不同符号表中的相同符号会合并.

但假如我们一个源文件里面,形成的符号表里面出现两个同样名字的Add函数,链接器不知道该如

何链接函数地址,此时就会发生报错.

C++则不会出现类似的问题,本质在于,它形成的符号表的名字会有相应的修饰,会根据这

个新的函数名字,去链接相应的函数地址.

简单验证下结论,我们将Add函数的定义直接改为声明,这样目标文件就无法链接,会发生相应的

报错.下图就是vs2019IDE的报错提示

可以看到,我们的Add函数,在符号表里面的名字,不再是单纯的函数名

而是变成了?Add@@YANNN@Z这个新的符号.

同样我们也可以在LINUX系统下进行验证

g++编译后,利用objdump + -S + 可执行程序,即可观察到对应函数符号.

原本的Add函数被修饰为_Z3Adddd这个新的符号.

linux下名字修饰规则也比较好懂,就是_Z+函数名字长度(3)+函数名(Add)+参数类型(d d)

知道规则,也可以明白为什么func1这个函数,在linux下会被修饰为_Z5func1id这个新符号.

PS:指针类型会加一个P,比如int* 对应为Pi

3.不同的函数重载类型

基于函数名的修饰原理,我们就可以进一步理解函数重载有什么类型.

1.参数类型不同

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;
}

2.参数个数不同

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

3.参数类型顺序不同

void f(int a, char b)
{
    cout << "f(int a,char b)" << endl;
}
void f(char b, int a)
{
    cout << "f(char b, int a)" << endl;
}

注意:返回类型不同,不能构成重载.

按照函数名修饰原则,两者符号是相同的,就算是不同的,那编译器又怎么知道用户调用的时候,想要返回哪个类型呢?

五.引用

1.概念介绍

在链表的实现时,尾插这个函数设计,我们需要传二级指针进去.

原因在于假如链表为空的时候,我们必须改变头指针,而想要改变一级指针,就必须传二级指针进

去.

类似的场景还有挺多,比如Swap函数的交换.

于是祖师爷为了提高程序的可读性和可写性,这次将目光投向了这里,并引入了引用的概念.

引用,通俗点讲,就是起别名.

比如李逵有很多外号,像黑旋风,铁牛等等.

那你无论是叫哪个称呼,都指的是同一个人.同样的道理,假如给a变量起了个别名就做pa,则无论

是变量a还是变量pa,指向的都是同一个人.

2.用法

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

3.应用特性

1.引用必须初始化使用

int a = 10;
int& b = a;//引用在定义时必须初始化

2.一个变量,可能有多个引用

#include <iostream>
using namespace std;
int main()
{
	//都是变量a
    int a = 1;
	int& pa = a;
	int& ppa = pa;
	int& k = a;
	return 0;
}

3.一旦引用了一个变量,就不能再引用其它变量

#include <iostream>
using namespace std;
int main()
{
	//Error
    int a = 1;
	int b = 2;
	int& pa = a;
	int& pa = b;
	return 0;
}

4.使用场景

1.输出型参数

形参的改变影响实参

//交换函数
void Swap(int& a, int& b)
{
	int tmp = a;
	a = b;
	b = tmp;
}

2.返回值

传引用返回

我们知道函数在调用结束后会被销毁,里面的内容,我们就没有相应权限进行访问.

这就类似于我们在酒店开了一间房间,住了一晚后,我们要退房,此时我们归还房间钥匙后,虽然

房间依旧存在,但我们就再也没有权利访问.

因此,函数如果有返回值,实际上并不是直接返回.

而是创建一个临时变量,将结果存进去,使用的时候,再把相应函数返回值拿出来.

(ps:根据返回值大小,临时变量存储的方式也不同    小:寄存器  大:main中提前开一块空间进

行存储临时变量)

这种我们称之为传值返回.

不同于传值返回,传引用返回,是将别名传回来,不需要创建临时变量.

因此我们也可以猜测到,传引用返回的变量,必须在函数销毁的时候,依旧存在,假如销毁了,那

传别名没有意义!

下面这段代码,就很好的诠释了这点.

#include <iostream>
using namespace std;
int& Add(int a, int b)
{
	int c = a + b;
	return c;
}
int main()
{
	int& ret = Add(1, 2);
	Add(3, 4);
	cout << "Add(1, 2) is :" << ret << endl;
	return 0;
}

由于c变量在函数栈帧销毁的同时,也一起被销毁,所以返回出的结果是未定义的,而并不是3.

传引用返回优势:

1.减少拷贝

2.调用者可以修改返回参数

如果函数返回时,出了函数作用域,返回对象还未还给系统,则可以使用引用返回;如果已经还给

系统了,则必须使用传值返回.

5.指针和引用的区别 

1. 引用概念上定义一个变量的别名,指针存储一个变量地址.

(引用的底层实现,依旧是指针)
2. 引用在定义时必须初始化,指针没有要求
3. 引用在初始化时引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何一个同类型实体

(引用不可以修改,而指针可以)
4. 没有NULL引用,但有NULL指针
5. 在sizeof中含义不同:引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32位平台下占4个字节)
6. 引用自加即引用的实体增加1,指针自加即指针向后偏移一个类型的大小
7. 有多级指针,但是没有多级引用
8. 访问实体方式不同,指针需要显式解引用,引用编译器自己处理

补充:两者的权限都可以缩小,但不能放大

const修饰的变量是不可以被修改值的,假如用指针或者引用,都需要const修饰,否则程序会报错.

#include <iostream>
using namespace std;
int main()
{
	const int a = 2;
	const int* pa = &a;
	const int& b = a;
	
	//Error
	const int a = 2;
	int* ppa = &a;
	int& b = a;
	return 0;
}

六.内联函数

简单编写一个Add的宏函数,相信很多初学者都会写错.

#define ADD ((x) + (y))

这也解释了宏的缺陷,容易写错,而且容易造成代码冗余,并且没得调试!

祖师爷于是引入了inline关键字,彻底解决宏函数问题.

在函数名前面加上inline,编译器在编译期间,会将我们的小程序用函数体直接替换,不再需要函

数调用,起到空间换时间的目的.

为什么说是空间换时间呢?

假如设计了一个swap函数,总共3行代码,在主函数中被调用了1000次.

如果没有inline,那单纯来看,由于被调用的是同一个函数,所以总的代码只有1003行.

如果引入inline,所有位置都会被替换为函数体,这时候代码就是3000行.

注意:

1.inline对于编译器而言只是一个建议,假如函数种存在递归,则还是会直接开辟函数栈帧调用.不同编译器关于inline实现机制可能不同

2.inline不建议声明和定义分离,分离会导致链接错误.因为inline被展开,就没有函数地址,无法实现链接.

3.支持在同一个项目中的不同源文件定义函数名相同但实现不同的inline函数,因为inline函数会在调用的地方展开,所以符号表中不会有inline函数的符号名,不存在链接冲突

七.auto(C++11)

随着程序越来越复杂,类型也会越来越复杂.

为了使程序变简洁,C++11引入auto关键字,可以自动识别变量类型.

#include <iostream>
using namespace std;
int main()
{
	int a = 10;
	auto pa = &a;   //自动推导出b的类型为int*
	auto* ppa = &a;  //自动推导出c的类型为int*
	auto& ap = a;   //自动推导出d的类型为int
	//打印变量b,c,d的类型
	cout << typeid(pa).name() << endl;//打印结果为int*
	cout << typeid(ppa).name() << endl;//打印结果为int*
	cout << typeid(ap).name() << endl;//打印结果为int
	return 0;
}

PS:auto不可用于识别数组类型.

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

借鉴其它语言的遍历数组,C++引入了基于范围的遍历

这是我们以前遍历数组的方式,利用sizeof来计算数组元素个数.

#include <iostream>
using namespace std;
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 i = 0; i < sizeof(array) / sizeof(array[0]); ++i)
		cout << array[i] << " ";
	cout << endl;
}
int main()
{  
	TestFor();
	return 0;
}

结合我们上面学习到的auto关键字,可以修改为下面的程序

for循环后的括号由冒号“ :”分为两部分:

第一部分是范围内用于迭代的变量

第二部分则表示被迭代的范围

其余和之前循环方式一样,可加break,可加continue等等

#include <iostream>
using namespace std;
void TestFor()
{
	int array[] = { 1, 2, 3, 4, 5 };
    //为了对原数组数据进行修改,要补上&
	for (auto& e : array)
		e *= 2;
	for (auto e : array)
		cout << e << " ";
	cout << endl;
}
int main()
{  
	TestFor();
	return 0;
}

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

下面看这段程序,按照我们的预想,传NULL指针的时候,应该输出Fun(int *)

#include <iostream>
using namespace std;
void Fun(int p)
{
	cout << "Fun(int)" << endl;
}
void Fun(int* p)
{
	cout << "Fun(int*)" << endl;
}
int main()
{
	Fun(0);           //打印结果为 Fun(int)
	Fun(NULL);        //打印结果为 Fun(int)
	return 0;
}

但是却是调用第一个Fun函数,这是为什么呢?

我们可以转到NULL的定义去看,如果__cplusplus被定义,NULL指针就等于0

因此,在c++眼里,NULL和0是没有区别的.

所以,我们还需要引入空指针nullptr,对应原来C中的NULL指针.

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值