初识C++编程语言(万字详解)

目录

::域作用限定符

命名空间域(namespace):

流插入和流提取(C++的输入输出)

缺省参数:

函数重载: 

引用:

内联函数:

auto关键字:

1、类型思考:

2、auto介绍: 

auto的使用细则 :

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

2. 在同一行定义多个变量

3、auto不能使用的场景 

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

1 、范围for的语法

 C++98中的指针空


::域作用限定符

在之前单纯的C语言中,同一个域里无法取得相同的名字这是很不方便的,在大型的工程中不同的程序员不可能做到所有变量都不相同,所以::域作用限定符应运而生。

例如:

 这种命名方式是不被允许的,在同一个域里,不能命名两个一样的变量。

#include<stdio.h>
int x = 0;
int main() {
	int x = 1;
	printf("%d\n", x);
	return 0;
}

所以我们可以向上面一样,将一个变量放在全局,一个放在局部。这样就不会报命名冲突的问题了,但是如果我们打印这个x变量,会发现屏幕上打印出来的是1。

为什么呢?🤔编译器会首先在局部域里找x,然后在去全局域里找,如果在局部域里找到了,自然就直接打印x了。

那么我们如果想要打印全局变量的这个0呢,我们可以这样:

#include<stdio.h>
int x = 0;
int main() {
	int x = 1;
	printf("%d\n", x);
	printf("%d\n", ::x);
	return 0;
}

 这时候就体现出了::的作用,如果::的左边什么都没有,那就是默认全局域,所以编译器会在全局域里找变量x。


命名空间域(namespace):

😎是为了解决命名空间里命名冲突的问题。

#include<stdio.h>

namespace project1 {
	int x = 1;
}
namespace project2 {
	int x = 2;
}

int main() {
	printf("%d\n", project1::x);
	printf("%d\n", project2::x);
	return 0;
}

我们如果想要命名同一个名字的话,我们就可以这样,用命名空间域来隔开,格式为:namespace+空间域名字。这样我们就可以用域空间操作符来指定使用变量名称了。

编译器搜索原则:

无指定:1、当前局部域  2、全局域  

有指定:1、直接在指定域中搜索

命令空间域里可以是:1、变量,2、函数,3、结构体。

#include<stdio.h>

namespace project1 {
	int x = 1;
}
namespace project2 {
    //变量
	int x = 2;
    //函数
	int Add(int a,int b){
		return a + b;
	}
    //结构体
	struct ListNode {
		/*....*/
	}LNode;
}

int main() {
	printf("%d\n", project1::x);
	printf("%d\n", project2::x);

	project2::Add(1,2);

	struct project2::ListNode phead;

	return 0;
}

需要注意的是:使用命名空间域里的结构体时,域名要在struct后结构体名前。

namespace还有一个使用方法:

当我们需要频繁调用project2时,就需要一直调用,就会变得很麻烦,所以我们介绍namespace的第二种用法:

这个叫做展开命名空间,也就是说使用了这个之后,project2就被展开了,被展开成了全局变量,再使用这个命名空间域里的东西的时候,就可以不用再引用了。

被展开的域相当于全局变量,所以当有局部变量的时候依然会优先访问局部变量。 

所以,当我们使用C++的时候,通常为了方便,会先写using namespace std,std就是C++的库,是为了方便调用C++库里面函数或者是关键字的。 (但是也仅仅是在我们平时的练习测试用的多,主要是为了方便)。

还有一种写法就是我可以展开这个库的一部分,也就是说:

就像这样,把几个常用的给展开,就可以了。

这时候有人要提出问题了,如果命名域的名字也冲突了怎么办?🤔

答:命名域允许嵌套,也就说如果命名域真的冲突了,我们也可以命明域进行嵌套来解决,而且如果我们想,我们可以无限的套娃下去。但一般我们嵌套两层就够用了。

可以看到这里我们在用的时候,就需要用两次::域作用限定符 。


流插入和流提取(C++的输入输出)

#include<iostream>
// std是C++标准库的命名空间名,C++将标准库的定义实现都放到这个命名空间中
using namespace std;
int main()
{
cout<<"Hello world!!!"<<endl;
return 0;
}

说明:
1. 使用cout标准输出对象(控制台)cin标准输入对象(键盘)时,必须包含< iostream >头文件
以及按命名空间使用方法使用std
2. cout和cin是全局的流对象,endl是特殊的C++符号,表示换行输出,他们都包含在包含< 
iostream >头文件中。
3. <<是流插入运算符,>>是流提取运算符
4. 使用C++输入输出更方便,不需要像printf/scanf输入输出时那样,需要手动控制格式。
C++的输入输出可以自动识别变量类型。

#include <iostream>
using namespace std;
int main()
{
   int a;
   double b;
   char c;
     
   // 可以自动识别变量的类型
   cin>>a;
   cin>>b>>c;
     
   cout<<a<<endl;
   cout<<b<<" "<<c<<endl;
   return 0;
}

// ps:关于cout和cin还有很多更复杂的用法,比如控制浮点数输出精度,控制整形输出进制格式等等。因为C++兼容C语言的用法,这些又用得不是很多,所以如果输出输入浮点数,我们还是采用C语言来实现 

std命名空间的使用惯例:
std是C++标准库的命名空间,如何展开std使用更合理呢?
1. 在日常练习中,建议直接using namespace std即可,这样就很方便。
2. using namespace std展开,标准库就全部暴露出来了,如果我们定义跟库重名的类型/对
象/函数,就存在冲突问题。该问题在日常练习中很少出现,但是项目开发中代码较多、规模大,就很容易出现。所以建议在项目开发中使用,像std::cout这样使用时指定命名空间 + 
using std::cout展开常用的库对象/类型等方式。


缺省参数:

#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(1,2,3);
	Fun(1,2);
	Fun(3);
	Fun();
	return 0;
}

 当我们来写一个函数的时候,如果我们传参了,那么就跟正常的一样,如果没传参,那么就会默认的用我们括号里面的值。

上面是缺省参数的四种形式,传参的时候如果我们想要修改其中的参数可以只传它的参数,没传参的就会默认括号里的值,但是不能进行跳跃传参,比如我们想只传b但是a,c还用默认值的话,这是办不到的。

还有一个是半缺省的形式, 像上面的b,我们可以不给默认值,但是这时候传参的话就必须传值了。

举一个实际的例子:

#include<iostream>
using namespace std;

struct Stack{
	int* a;
	int size;
	int capacity;
};
void StackInit(struct Stack* ps) {
	//ps->a = NULL;
	ps->a = (int*)malloc(sizeof(int) * 100);
}

int main() {
	struct Stack st1;
	StackInit(&st1);
	return 0;
}

我们假设初始化栈的时候,开辟栈的大小使用malloc,但是当我们有以下三种情况的时候:

这种情况下我们又怎么进行动态开辟呢,大小该给多少呢?

这时候我们的缺省参数就派上用场了:

#include<iostream>
using namespace std;

struct Stack{
	int* a;
	int size;
	int capacity;
};
void StackInit(struct Stack* ps ,int n = 4) {
	//ps->a = NULL;
	ps->a = (int*)malloc(sizeof(int) * n);
}

int main() {
	struct Stack st1;
	//确定要插入100个数据
	StackInit(&st1,100);

	struct Stack st2;
	//确定要插入10个数据
	StackInit(&st2,10);

	struct Stack st3;
	//不知道要插入多少的时候
	StackInit(&st3);

	return 0;
}

 这样是不是就很方便呢。

那当声明与定义分离的时候,该在哪里给缺省参数呢?

答案是:声明。                                                    
首先声明和定义肯定是不能同时给缺省参数的,因为当两个缺省参数不同时,就会报错。

其次,缺省参数是在调用的时候用的,那么将缺省参数放在声明才不会在编译的时候报错。

最后,缺省值必须是常量或者全局变量。


函数重载: 


C语言中我们不能命名相同名字的函数,在C++中我们可以命名相同函数的名字,但前提是:函数的参数的个数,类型,顺序不一样。

C语言做不到的,C++又是如何做到的呢?

C++明确了在调用的时候,不仅仅可以用函数的名字,还可以加上函数的参数类型合并起来在进行判断是否是重名的,并且还加上了参数数量的比较,就比如上面的两个Add,他们在编译完之后一个是Addci,另一个是Adddd。所以最后他们就是不同的函数。

但这里只是一个例子,不同的编译器会有不同的实现方式,但是思路都是一样的。


引用:

C语言中,我们如果想要指向一个变量的话,我们会将他的地址取出来存到指针变量里面,但是在针对一些复杂指针的时候,就很不方便,所以C++采用了引用。

#include<iostream>
using namespace std;
int main() {
	int a = 1;
	//引用
	int& b = a;
	cout << a << endl << b << endl;
	return 0;
 }

引用的形式就是:&(取地址符),如上面代码int& b = a,此时b就是a的别称,例如周树人的笔名是鲁迅,那么鲁迅和周树人都是指同一个人。所以引用的作用,就是给a取了一个别名b,那么无论输出a还是输出b,结果都是1。

#include<iostream>
using namespace std;
//这是我们C语言中的交换函数,必须传递实参的地址。
void Swap(int* x, int* y) {
	int tmp = *x;
	*x = *y;
	*y = tmp;
}
//这是用引用的写法,这里的x,y就是两个实参的别名
void Swap(int& x, int& y) {
	int tmp = x;
	x = y;
	y = tmp;
}
int main() {
	int a = 1;
	int c = 5;
	cout << a <<"<->" << c << endl;
	//Swap(a, c);
	Swap(&a, &c);
	cout << a << "<->" << c << endl;
	Swap(a, c);
	cout << a <<"<->" << c << endl;
	return 0;
 }

交换函数正常的实现,这就是引用的用法。

注意事项:

1、引用必须初始化,不可以写成:

int& b;
b = c;

 2、引用定义后不能改变指向,不能写成:

int& b = a;
int c = 3;
b = c; //这里就不是改变指向,而是赋值

3、 一个变量可以有多个引用,多个别名

int main() {
	int a = 1;
	int& b = a;
	int& d = a;
	int& e = a;
	return 0;
 }

这里面的bde都是a的别名。

既然引用的作用与指针作用重叠。

那么有小伙伴要提问了,那引用不是就替代指针了?🤔🤔🤔

 C++的引用是为了解决指针使用复杂场景的一些替换,让代码更简单易懂,但不能完全代替。

引用不能替代指针的原因:引用定义后不能改变指向。

例如:

 在双向链表中,删除中间的结点,该删除结点的前一个和后一个要改变指向,这是引用做不到的。

引用还有更为广泛的应用就是:

void PushBack(struct ListNode** phead,int x) {
	//........
}
void PushBack(struct ListNode*& phead, int x) {
	//........
}
int main() {
	struct ListNode* pList = NULL;
}

假如我们在PushBack函数里要对指针进行修改,那么我们就必须传一个二级指针(如第一个写法),那有了引用之后(第二个写法)ListNode*phead就是ListNode*pList的一个别名,所以当然可以在PushBack函数进行修改了。

还有一种经常出现在书上的写法让一些刚学C语言的小伙伴们十分头疼:

typedef struct ListNode {
	ListNode* prev;
	ListNode* next;
	int val;
}LNode, *PNode;

void PushBack(PNode& phead, int x) {
	//........
}
int main() {
	PNode pList = NULL;
	return 0;
}

这个*PNode就是struct ListNode的指针,所以指针类型就直接是PNode,然后再结合上&引用,就实现了这个的参数类型的写法。

然后我们来一个简易的顺序表感受一下C和C++的不同:

#include<iostream>
#include<assert.h>
using namespace std;

struct SeqList {
	int* a;
	int size;
	int capacity;
};
void SLInit(SeqList& sl) {
	sl.a = (int*)malloc(sizeof(int) * 4);
	sl.size = 0;
	sl.capacity = 4;
}
void SLModity(SeqList& sl, int pos,int x) {
	assert(pos >= 0);
	assert(pos < sl.size);
	sl.a[pos] = x;
}
void SLPushBack(SeqList& sl, int x) {
	sl.a[sl.size++] = x;
}
int SLGet(SeqList& sl, int pos) {
	assert(pos >= 0);
	assert(pos < sl.size);
	return sl.a[pos];
}
int main() {
	SeqList sl;
	SLInit(sl);
	SLPushBack(sl, 1);
	SLPushBack(sl, 2);
	SLPushBack(sl, 3);
	SLPushBack(sl, 4);
	for (int i = 0; i < sl.size; i++) {
		cout << SLGet(sl, i) <<" ";
	}
	cout << endl;
	for (int j = 0; j < sl.size; j++) {
		int val = SLGet(sl, j);
		if (val % 2 == 0) {
			SLModity(sl, j, val * 2);
		}
	}
	for (int i = 0; i < sl.size; i++) {
		cout << SLGet(sl,i) << " ";
	}
	return 0;
 }

这个代码嵌了一点点C++,但是跟C差不多。

#include<iostream>
#include<assert.h>
using namespace std;

struct SeqList {
	int* a;
	int size;
	int capacity;
	void Init() {
		a = (int*)malloc(sizeof(int) * 4);
		size = 0;
		capacity = 4;
	}
	void PushBack(int x) {
		a[size++] = x;
	}
	int& Get(int pos) {
		assert(pos >= 0);
		assert(pos <= size);
		return a[pos];
	}
};

int main() {
	SeqList sl;
	sl.Init();
	sl.PushBack(1);
	sl.PushBack(2);
	sl.PushBack(3);
	sl.PushBack(4);
	for (int i = 0; i < sl.size; i++) {
		cout << sl.Get(i) <<" ";
	}
	cout << endl;
	for (int j = 0; j < sl.size; j++) {
		if (sl.Get(j) % 2 == 0) {
			sl.Get(j)*= 2;
		}
	}
	for (int i = 0; i < sl.size; i++) {
		cout << sl.Get(i) << " ";
	}
	return 0;
 }

这里如果用int不加引用的话,相当于返回的是一个临时拷贝,临时变量是建立在堆上的,并且临时变量具有常性(可以理解为被const修饰过的),那么我们肯定是没办法修改的。就算这里不考虑常性,那修改的也只是这个临时变量,并没有修饰到顺序表中的本体,但是加了引用之后,不产生临时变量,引用就相当于返回了他的别名,那么修改别名就是修改了本体。所以用引用返回值具有有了读写功能。

引用和指针的区别:

语法层面:

1、引用是别名,不开空间,指针是地址需要开空间。

2、引用必须初始化,指针可以初始化也可以不初始化。

3、引用不能改变指向,指针可以。

4、引用相对更安全,没有空引用,但是空指针,容易出现野引用,但是不容易出现野指针。

5、sizeof、++、解引用访问等上面的区别。

底层: 

汇编层面上,没有引用只有指针,编译后引用也变成了指针。

在汇编层面上,引用和指针是没有区别的。 


内联函数:

C语言调用函数是需要开销的,假如说C语言有一个频繁调用的小函数:

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

假如这个相加的小函数需要调用100w次,那就需要建立100w个栈帧空间。

那C语言是如何解决这个问题的:宏函数。

#define Add(a,b) ((a)+(b))

但是宏有很多缺点:

1、语法复杂,坑较多,不容易控制。

2、不能调试。

3、没有安全类型的检查。 

所以C++引出了内联函数:inline

inline int Add(int a, int b) {
	return a + b;
}

他的功能其实是和宏差不多,在多次调用的时候,不需要每次都建立栈帧。

并且优化了宏的缺点 。

inline函数的特性:

1、内联函数是一种空间换时间的做法,如果编译器把函数当作内联函数使用的时候,在编译过程中,编译器会把函数体替换为函数调用。缺点:函数体展开使目标文件变大。(当函数体较长,再去多次调用多次展开,会出现代码膨胀)优点:减少函数调用开销,提高代码运行效率。

2、将函数规模较小(即函数不是很长,具体没有准确的说法,取决于编译器内部实现)、不是递归、且频繁调用的函数采用inline修饰。

3、inline不建议声明和定义分离,分离会导致链接错误。因为inline被展开,就没有函数地址
了,链接就会找不到。

// F.h
#include <iostream>
using namespace std;
inline void f(int i);
// F.cpp
#include "F.h"
void f(int i)
{
 cout << i << endl;
}
// main.cpp
#include "F.h"
int main()
{
 f(10);
 return 0;
}
// 链接错误:main.obj : error LNK2019: 无法解析的外部符号 "void __cdecl 
//f(int)" (?f@@YAXH@Z),该符号在函数 _main 中被引用

在C++中是比较排斥使用宏的,一般会采用以下方法来替代宏:

1、常量定义换用const、enum

2、短小函数定义换用inline


auto关键字:

1、类型思考:

随着我们学习的深入,变量的类型会越来越复杂,这就导致我们在使用的时候变得十分麻烦。

#include <string>
#include <map>
int main()
{
 std::map<std::string, std::string> m{ { "apple", "苹果" }, { "orange", "橙子" }, 
   {"pear","梨"} };
 std::map<std::string, std::string>::iterator it = m.begin();
 while (it != m.end())
 {
 //....
 }
 return 0;
}

std::map<std::string, std::string>::iterator 是一个类型,但是该类型太长了,特别容易写错。

2、auto介绍: 

auto 作为一个新的类型指示符来指示编译器,auto声明的变量必须由编译器在编译时期推导而得。言简意赅就是auto可以根据变量的值自动推导变量的类型。

int TestAuto()
{
return 10;
}
int main()
{
int a = 10;
auto b = a;
auto c = 'a';
auto d = TestAuto();
cout << typeid(b).name() << endl;
cout << typeid(c).name() << endl;
cout << typeid(d).name() << endl;
//auto e; 无法通过编译,使用auto定义变量时必须对其进行初始化
return 0;
}

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

auto的使用细则 :

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

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

2. 在同一行定义多个变量

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

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

3、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) 

1 、范围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循环后的括号由冒号“ :”分为两部分:第一部分是范围内用于迭代的变量,第二部分则表示被迭代的范围。

void TestFor()
{
int array[] = { 1, 2, 3, 4, 5 };
for(auto& e : array)
     e *= 2;
for(auto e : array)
     cout << e << " ";
return 0;
}

 范围for相当于是依次取数组里面的值赋值给e,然后再将e输出,auto也是能更好的自动识别类型,以应对各种类型的数组,&是为了如果想要改变数组内容,那么我们引用就把e变成了数组每个元素的别名,就可以修改数组里面的值了。

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

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

 上面这个代码是不能用范围for的,现阶段范围for只能用于数组,但是在上面的TestFor函数中,array不是数组,而是一个指针。


 C++98中的指针空值

在良好的C/C++编程习惯中,声明一个变量时最好给该变量一个合适的初始值,否则可能会出现不可预料的错误,比如未初始化的指针。如果一个指针没有合法的指向,我们基本都是按照如下方式对其进行初始化:

void TestPtr()
{
int* p1 = NULL;
int* p2 = 0;
// ……
}

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

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

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

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

 程序本意是想通过f(NULL)调用指针版本的f(int*)函数,但是由于NULL被定义成0,因此与程序的初衷相悖。
在C++98中,字面常量0既可以是一个整形数字,也可以是无类型的指针(void*)常量,但是编译器默认情况下将其看成是一个整形常量,如果要将其按照指针方式来使用,必须对其进行强转(void *)0。

 所以从C++11以来用nullptr来填上了这个小坑。

#include<iostream>
using namespace std;
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);
	int* p = nullptr;
	return 0;
}

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


评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值