C++_从C到C++

从这篇博客开始,我将进入对C++的又一次学习。正如名字一样,C++是C的升级和扩充,C++兼容了C的几乎所有语法,在C的基础上增加了许多有用的库,并且增加了面向对象编程的思想。C++是在C的基础上创造出的新的语言,学习C++就必须要搞清楚从C到C++有哪些保留和改变,首先得明白基础语法的区别。这篇博客从基础语法的角度,梳理从C到C++的语法的变化。

命名空间

命名冲突问题

  1. 自己定义的变量、函数名很容易和库里面的名字重复
  2. 项目比较大时,容易和别人的代码之间命名冲突

C语言没法解决这个问题,C++提出了新的语法——命名空间namespace
简单来说,命名空间是为了避免命名冲突或者名字污染。创建一个命名空间,就是创建了一个作用域,出了作用域命名空间中内容都不可以使用,同样的,我们要使用命名空间中的内容,直接去命名空间中找就行。如此,我们定义的名字跟库中哪怕重复了,也不会相互影响。

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

// 定义了一个叫ZWK的命名空间 -- 一个域
namespace ZWK
{
	// 全局变量放在静态区,命名空间里面可以定义变量,但是不能赋值,和函数的域不一样
	int rand = 10;
}

int a = 0;

int main()
{

	printf("Hello, world!\n");
	printf("%d\n", rand);
	printf("%d\n", ZWK::rand); // ::-作用域限定符

	int a = 1;
	printf("%d\n", a); // 就近原则,打印1 
	printf("%d\n", ::a); // ::左边为空,说明去全局域寻找a,打印0

	return 0;
}
  1. 命名空间中可以定义变量/函数/类型
// 命名空间中可以定义变量/函数/类型
namespace ZWK
{
	int a = 0;
	int b = 1;

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

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

int main()
{
	printf("%d\n", ZWK::a);

	struct ZWK::Node node;

	ZWK::Add(1, 2);

	return 0;
}
  1. 命名空间可以嵌套定义
// 命名空间可以嵌套定义
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;
		}
	}

}

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

	int c = N1::Add(a, b);
	int d = N1::Add(N1::a, N1::b);

	int e = N1::N2::Sub(a, b);

	printf("%d %d %d", c, d, e);

	return 0;
}
  1. 同一个工程中允许存在多个相同名称的命名空间,编译器最后会合成同一个命名空间中。
// 一个工程中的test.h和上面test.cpp中两个N1会被合并成一个
// test.h
namespace N1
{
	int Mul(int left, int right)
	{
		return left * right;
	}
}

命名空间的使用
命名空间有三种使用方式(用ZWK命名空间举例)

  1. 加命名空间或者作用域限定符
    ZWK::a
  2. 展开命名空间中常用的变量
    using ZWK::Node
  3. 全部展开。用起来方便,但是隔离失效了 最好别用
    using namespace ZWK

具体见下方代码:

namespace ZWK
{
	int a = 0;
	int b = 1;

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

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

//单独展开变量struct Node (方式二)
using ZWK::Node;

int main()
{
	printf("%d", ZWK::a); // 方式一

	Node node;  // 前面展开Node后,可以不再写成struct ZWK::Node node,c++里面也可以省略struct
	node.val = 1;
	printf("%d", node.val);

	return 0;
}

C++输入&输出

在C语言中,我们的输入、输出一般是使用scanfprintf,在使用之前我们会包含它们的头文件stdio.h
在C++中引入了新的输入、输出方式:cincout,例如,输入变量a的方式为cin >> a;,输出变量a的方式为:cout << a << endl;。其中,endl是换行的意思,>>是流提取符号,<<是流插入符号。同样的我们在使用它们之前需要包含它们的头文件iostream。与C语言有所区别的是,因为C++中引入了命名空间规则,cin、cout写在命名空间std中,因此在使用他们之前需要展开。上面我们说了,命名空间展开有三种方式,针对我们要使用的cin、cout会有以下三种具体使用方法:

  1. 使用作用域限定符。
    std::cout << a;std::cin >> a;
  2. 单独展开
    using std::cout;
    using std::cin;
    using std::endl;
  3. 全部展开
    using namespace std;

在日常练习C++时,为了方便,我们直接使用的三种方式就行,对性能影响不大。

#include<iostream>

//using namespace std; 
// C++库的实现写在名叫std的命名空间中,这里就是展开这个库,但是这样写不好,会展开太多的内容
// 所以项目中最好在下面使用命名空间+作用域限定符的方式写

// 下面的写法可以避免展开过多内容,稍麻烦,平常练习写代码可以直接用using namespace std
using std::cout;
using std::endl;
using std::cin;

int main()
{
	cout << "Hello World!" << endl;
	cout << "Hello World!\n";
	cout << "Hello World!"<<'\n';

	int i = 10;
	double d = 1.11;
	// cout cin 可以自动识别类型
	cout << i << " hhh " << d << endl;
	cin >> i >> d;
	cout << i << " hhh " << d << endl;


	return 0;
}

如何选择输出方式? 怎么方便怎么来,printf和cout都可以用

struct Stu
{
	char name[10];
	int age;
};
int main()
{
	// << 流插入运算符
	const char* str = "hello";
	cout << str << endl;
	printf("hello\n");

	Stu s;
	// >> 流提取运算符
	cin >> s.name >> s.age;
	cout << "姓名:" << s.name << endl;
	cout << "年龄:" << s.age << endl;
	printf("姓名:%s\n年龄:%d", s.name, s.age);
	
	return 0;
}

参数缺省

函数定义的时候,给形参一个默认值,这样在调用函数的时候就可以不输形参

// 缺省参数
void Func_1(int a = 0)
{
	cout << a << endl << endl;
}

// 全缺省 -- 就是所有形参都有默认值
void Func_2(int a = 10, int b = 20, int c = 30)
{
	cout << a << endl;
	cout << b << endl;
	cout << c << endl << endl;
}

// 半缺省 -- 部分参数有默认值 -- 必须从右往左给默认值,不能间隔着给
void Func_3(int a, int b = 20, int c = 30)
{
	cout << a << endl;
	cout << b << endl;
	cout << c << endl << endl;
}

int main()
{
	Func_1(10);
	Func_1();

	Func_2();
	Func_2(1,2);

	Func_3(1);

	return 0;
}

注意:

  1. 半缺省参数必须从右往左依次来给出,不能间隔着给
  2. 缺省参数不能在函数声明和定义中同时出现。要么出现在定义,要么出现在声明,推荐写在声明中
//a.h
void Func(int a = 10);
// a.cpp
void Func(int a = 20)
{}
// 注意:如果生命与定义位置同时出现,恰巧两个位置提供的值不同,那编译器就无法确定到底该用哪个缺省值。
  1. 缺省值必须是常量或者全局变量
  2. C语言不支持(编译器不支持)

参数缺省的使用场景

定义栈时使用缺省参数,如果我们知道栈中要存多少数据,那么我们就自己定义容量大小(可以避免反复增容带来的消耗),如果不知道,那就使用默认值。

函数重载

自然语言中,一个词可以有多重含义,人们可以通过上下文来判断该词真实的含义,即该词被重载了。比如:以前有一个笑话,国有两个体育项目大家根本不用看,也不用担心。一个是乒乓球,一个是男足。前者是“谁也赢不了!”,后者是“谁也赢不了!”

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

只有以下三种情况构成重载

// 函数重载

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

int main()
{
	Add(10, 20);
	Add(10.1, 20.2);
	f();
	f(10);
	f(10, 'a');
	f('a', 10);
	return 0;
}

注意:参数是否缺省,不影响是否重载

为什么C++可以重载而C语言不能重载?

C语言在编译的时候,两个重载函数的函数名相同,在.o中的符号表中存在歧义和冲突,其次链接的时候也存在歧义和冲突,因为他们都是使用函数名去标识和查找,而重载函数的函数名相同。

而C++的目标文件符号表中不是直接用函数名来标识和查找函数。C++具有函数名修饰规则,即重载函数在.o中的符号表中生成的指令是不同的,这样就不存在歧义了。链接过程中调用重载函数时,通过地址来寻找函数时也是明确的。

引用

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

引用定义

int main()
{
	int a = 10;
	// 引用定义 - 引用在语法层没有开新空间,就是对原来的空间取了一个新名称
	int& b = a;

	// 取地址
	int* p = &b;

	b = 30;

	return 0;
}

引用语法

int main()
{
	// 1. 引用在定义时必须初始化
	int a = 10;
	int& b = a;

	// 2. 一个变量可以有多个引用,也可以给别名取别名
	int aa = 10;
	int& bb = aa;
	int& cc = aa;
	int& dd = cc; // 给别名取别名

	// 3. 引用一旦引用了一个实体,就不能再引用其它实体
	int aaa = 10;
	int& bbb = aaa;

	int c = 20;

	bbb = c; // 在这里,相当于赋值,而不是让b成为c的别名,引用是从一而终的,指针像极了渣男

	cout << bbb << endl;

	return 0;

}

引用的作用

  1. 引用做参数 - a.提高效率,减少拷贝带来的消耗 b.形参的修改可以影响到实参
// 引用的作用
// 1. 引用做参数
// 传引用
void swap(int& p1, int& p2)
{
	int tmp = p1;
	p1 = p2;
	p2 = tmp;
}
// 传值
void swap(int r1, int r2)
{
	int tmp = r1;
	r1 = r2;
	r2 = tmp;
}

// 传指针 - C语言中的交换函数写法
void swap(int* r1, int* r2)
{
	int tmp = *r1;
	*r1 = *r2;
	*r2 = tmp;
}

// 上面三个函数构成重载(函数修饰名是不同的),但是调用的时候会产生歧义,所以编译器会报错

int main()
{
	int x = 0, y = 1;
	swap(x, y); // 既不传值,也不传地址,而是传引用
	cout << x << "  " << y << endl;

	swap(&x, &y); // C语言中的做法
	cout << x << "  " << y << endl;

	return 0;
}

注意:在另一个作用域中,可以给实参起跟实参相同的别名,因为是处于不同栈帧的。

// 指针的别名
int main()
{
	int a = 10;
	int* p = &a;
	int*& q = p; // q就是指针变量p的别名

	cout << a << " " << *p << " " << *q << endl;
		 
	return 0;
}
  1. 引用做返回值 - a、提高效率 b、修改返回对象,实现可读可写

注意:如果函数返回时,出了函数作用域,如果返回对象还在(还没还给系统),则可以使用引用返回,如果已经还给系统了,则必须使用传值返回。

// 引用做返回值

// 传值(拷贝)返回
/*
	临时变量c的值存在哪?
	如果c比较小(4 or 8 字节),那么存在寄存器中
	如果c比较大(结构体等),那么存在调用层的栈帧中
*/
int Add_1(int a, int b)
{
	int c = a + b;
	return c;
}


// 传引用返回
int& Add_2(int a, int b)
{
	int c = a + b;
	return c;
}

int main()
{
	int ret = Add_2(1, 2);
	cout << ret << endl;
	/*
		此时的代码是有问题的,因为传回的是c的别名,相当于把c传回来了,但是c是个临时变量,调用完之后函数栈帧会被销毁
		vs编译器下虽然销毁了函数栈帧,但是并没有清理数据,所以是可以输出ret的,但是如果在Add函数调用完之后又调用了其它函数,那么有可能其他函数开辟的栈帧在销毁后的Add的栈帧的位置,那么有可能会覆盖c的位置,所以可能会是是随机值。
		vs2019编译器好像避免了这个问题
	*/
	return 0;
}

返回值为引用举例

int& func(int a, int b)
{
	int* p = (int*)malloc(sizeof(int) * 1);
	*p = a + b;

	return *p;
}

int main()
{
	int a = 10;
	int b = 20;

	int c = func(a, b);

	cout << c << endl;

	return 0;
}

可读可写举例

// 可读可写的实现 - 既能修改返回值,也能读取返回值,还能控制输出哪个返回值
int& at(int i)  // 去掉引用符号,返回值是一个临时变量,临时变量具有常性
{
	static int a[10];

	return a[i];
}

int main()
{
	for (int i = 0; i < 10; i++)
	{
		at(i) = 10 + i; // 返回的是a[i]的别名,所以就相当于为a[i]赋值
	}

	for (int i = 0; i < 10; i++)
	{
		cout << at(i) << " "; // 输出a[i]
	}
	cout << endl;

	return 0;
}
  1. 引用传值
// 以下代码演示了传引用的优点(不需要拷贝实参)
#include <time.h>
struct A 
{ 
	int a[10000];
};

void TestFunc1(A a) 
{}
void TestFunc2(A& a) 
{}

void TestRefAndValue()
{
	A a;

	// 以值作为函数参数
	size_t begin1 = clock();
	for (size_t i = 0; i < 10000; ++i)
		TestFunc1(a);
	size_t end1 = clock();

	// 以引用作为函数参数
	size_t begin2 = clock();
	for (size_t i = 0; i < 10000; ++i)
		TestFunc2(a);
	size_t end2 = clock();

	// 分别计算两个函数运行结束后的时间
	cout << "TestFunc1(A)-time:" << end1 - begin1 << endl;
	cout << "TestFunc2(A&)-time:" << end2 - begin2 << endl;
}

int main()
{
	TestRefAndValue();

	return 0;
}

常引用

// 常引用
int main()
{
	// 权限放大 不行
	//const int a = 10;
	//int& b = a;

	// 权限不变 可以
	const int a = 10;
	const int& b = a;

	// 权限缩小 可以
	int c = 10;
	const int& d = c;
    
   	// 不同类型变量之间的引用
	double d = 11.11;
	// int& d1 = d; 这样不行
	const int& d2 = d; // 这样可以 
	// 因为类型不同,存在整型提升,所以先把d拷贝给一个临时变量,然后再给临时变量取别名为d2,临时变量为右值,具有常性,所以加const就可以

	return 0;
}

注意

  • 临时变量为右值,具有常性。

  • 不同类型变量之间赋值或者比较大小,都不是一次性完成的,而是先整型提升,存到一个临时变量中,然后再赋值。

  • 右值一般是不能修改的值,比如说表达式的返回值、临时变量

  • const Type& 可以接收所有类型

指针和引用的区别

  1. 引用在概念上就是一个变量的别名 vs 指针存储一个变量的地址
  2. 引用在定义时必须初始化 vs 指针最好初始化,但是不初始化也没错(也可以编译过)
  3. 引用定义是初始化引用一个实体后不能再引用其他实体 vs 指针可以任意指向同类的一个实体。(引用从一而终,指针像极了渣男)
  4. 没有NULL引用 vs 有NULL指针
  5. 在sizeof中:引用的结果为引用类型的大小 vs 指针的大小始终为4个字节(32)或者8个字节(64)
  6. 引用自加是引用的实体自加 vs 指针自加是向后偏移一个所指向类型大小的位置
  7. 引用比指针安全一点,指针的使用更危险。

引用与指针

int main()
{
	int a = 10;

	int& b = a;

	int* p = &a;

	b = 2;
	*p = 3;

	cout << a << endl;
	cout << b << endl;
	cout << *p << endl;
	// a b *p 都是同一个值

	cout << &a << endl;
	cout << &b << endl;
	cout << p << endl;
	// &a &b p 都是同一个地址

	return 0;
}

内联函数

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

内联函数的作用

不使用内联函数时:
如果多次调用Add函数,就需要多次开辟栈帧,消耗巨大。

使用内联函数时:
如果使用了内联函数,编译器会将Add函数在使用的地方展开,不需要开辟栈帧。

有了内联就可以不使用宏,使用安全性较高(指不容易出错)。

内联函数的特性

  1. inline是一种以空间换时间的做法,如果编译器将函数当成内联函数处理,在编译阶段,会用函数体替换函数调用,缺陷:可能会使目标文件变大,优势:少了调用开销,提高程序运行效率。

  2. inline对于编译器而言只是一个建议,不同编译器关于inline实现机制可能不同,一般建议:将函数规模较小(即函数不是很长,具体没有准确的说法,取决于编译器内部实现)、不是递归、且频繁调用的函数采用inline修饰,否则编译器会忽略inline特性。

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

auto关键字(C++11)

auto简介

C++11中,标准委员会赋予了auto全新的含义即:auto不再是一个存储类型指示符,而是作为一个新的类型指示符来指示编译器,auto声明的变量必须由编译器在编译时期推导而得 。

总结来说:随着程序越来越复杂,程序中使用的类型越来越复杂,类型名很长,写起来很麻烦,于是有了auto根据变量来自动推导类型。

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

auto可以自动推导类型

int main()
{
	const int a = 0;
	int b = 0;

	// 自动推导c的类型
	auto c = a;
	auto d = 'A';
	auto e = 11.11;
	cout << typeid(c).name() << endl;
	cout << typeid(d).name() << endl;
	cout << typeid(e).name() << endl;

	// 实际中我们不会像上面那样取用auto
	// 实际中使用场景
	std::map<std::string, std::string> dict = { {"sort", "排序"} ,{"insert" , "插入"} };
	//std::map<std::string, std::string>::iterator it = dict.begin();
	
	// auto会根据右边返回值自动去推导it的类型,写起来就方便了
	auto it = dict.begin();

	return 0;
}

auto使用细则

  1. auto与指针和引用结合起来使用
    用auto声明指针类型时,用auto和auto*没有任何区别,但用auto声明引用类型时则必须加&
  int main()
  {
  	int x = 10;
  	auto a = &x; // int*
  	auto* b = &x; // int* 这块体现了auto推类型的灵活
  	auto& c = x; // int
  
  	cout << typeid(a).name() << endl;
  	cout << typeid(b).name() << endl;
  	cout << typeid(c).name() << endl;
  
  	*a = 20;
  	*b = 30;
  	c = 40;
  
  	return 0;
  }
  1. 在同一行定义多个变量
    当在同一行声明多个变量时,这些变量必须是相同的类型,否则编译器将会报错,因为编译器实际只对第一个类型进行推导,然后用推导出来的类型定义其他变量。
 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[] = {456};
    }
    
  3. 为了避免与C++98中的auto发生混淆,C++11只保留了auto作为类型指示符的用法

  4. auto在实际中最常见的优势用法就是跟以后会讲到的C++11提供的新式for循环,还有lambda表达式等进行配合使用

语法糖 - 范围for

int main()
{
	// 语法糖 -- 范围for
	int array[] = { 1,2,3,4,5 };

	// C/C++遍历数组
	for (int i = 0; i < sizeof(array) / sizeof(array[0]); i++)
	{
		cout << array[i] << " ";
	}
	cout << endl;

	// C++11 

	// 自动给array中元素取别名,然后自加1
	for (auto& e : array)
	{
		e++;
	}

	// 自动取array中的每个元素,赋给e(自己取名,也可以自己取类型),自动判断结束
	for (auto e : array)
	{
		cout << e << " ";
	}

	return 0;
}

注意

void TestFor(int array[]) // 数组传参时数组会退化为int* 类型,而范围for中必须使用数组名
{
	for(auto& e : array)
	cout<< e <<endl;
}

指针空值nullptr(C++11)

C++使用nullptr的原因: 应对极端情况下的函数重载,见下面代码:

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

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

int main()
{
	// C++98/03 -- NULL预编译结束后就是0
	int* p1 = NULL;
	int* p2 = 0;
	f1(0);
	f1(NULL);
	/*
		因为C++可以函数重载,本意想调用两个函数,但是由于预编译之后两个调用都变成了f(0),因此无法实现
		所以C++11中提出了新的指针空值
	*/

	// C++11 -- nullptr
	int* p3 = nullptr;

	return 0;
}

注意:

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值