C++第一弹 -- C++基础语法上(命名空间 输入输出 缺省参数 函数重载 引用)

目录

前言

C++是在C的基础之上,容纳进去了面向对象编程思想,并增加了许多有用的库,以及编程范式等。熟悉C语言之后,对C++学习有一定的帮助,本篇将介绍C++是如何对C语言设计不合理的地方进行优化的,比如:作用域方面、IO方面、函数方面、指针方面、宏方面等, 为后续类和对象学习打基础

更多好文点击 博客主页: 酷酷学!!! 期待您的关注!


正文开始

一. C++关键字(C++98)

C++总计63个关键字,C语言32个关键字

ps:下面我们只是看一下C++有多少关键字,不对关键字进行具体的讲解。后面我们学到以后再细讲。

二. 命名空间

在C/C++中,变量、函数和后面要学到的类都是大量存在的,这些变量、函数和类的名称将都存在于全局作用域或者局部作用域中,可能会导致很多冲突。使用命名空间的目的是对标识符的名称进行本地化,以避免命名冲突或名字污染namespace关键字的出现就是针对这种问题的。

使用命名空间也可以很好的解决我们和库之间或者多个程序员在进行一个项目时, 名称重复的问题, 如下面这个例子

c 复制代码
#include<stdio.h>
#include<stdlib.h>

int rand = 10;

int main()
{
	printf("%d\n", rand);
	return 0;
}

这里会报错, 因为rand已经在<stdlib.h>头文件中定义过, 所以再次定义全局变量rand, 作用域都是全局, 会导致名称重复, C语言没办法解决类似这样的命名冲突问题,所以C++提出了namespace来解决.

编译后后报错:error C2365: "rand": 重定义;以前的定义是"函数"

1.命名空间的定义

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

注意: 这里和结构体的区别在于结构体{}后有; 而命名空间没有

  1. 正常的命名空间的定义

命名空间可以定义变量/函数/类型

c 复制代码
#include<stdio.h>
#include<stdlib.h>

//int rand = 10;

namespace kkx
{
	int rand = 10;

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

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

int main()
{
	//int rand = 20;
	printf("%d\n", kkx::rand);
	return 0;
}
  1. 命名空间可以嵌套
c 复制代码
//test.cpp
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. 同一个工程中允许存在多个相同名称的命名空间,编译器最后会合成同一个命名空间中.

PS:一个工程中的test.h和上面的test.cpp中两个N1会被合并成一个

c 复制代码
//test.h
namespace N1
{
	int Mul(int left, int right)
	{
		return left * right;
	}
}

2.命名空间的使用

命名空间中成员该如何使用呢?比如:

c 复制代码
namespace N
{
	int a = 10;
	int b = 1;
	int Add(int left, int right)
	{
		return left + right;
	}

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

int main()
{
	printf("%d\n", a);
	return 0;
}
// 编译报错:error C2065: "a": 未声明的标识符

编译默认查找

a、当前局部域 : 自留地

b、全局域找 : 村子野地

不会到其他命名空间中去找

命名空间的使用有三种方式:

  • 加命名空间名称及作用域限定符
c 复制代码
int main()
{
    printf("%d\n", N::a);
    return 0;    
}
  • 使用using将命名空间中某个成员引入
c 复制代码
using N::b;
int main()
{
    printf("%d\n", N::a);
    printf("%d\n", b);
    return 0;    
}
  • 使用using namespace 命名空间名称 引入

编译默认查找

/a、当前局部域 : 自留地

b、全局域找 : 村子野地

c、到展开的命名空间中查找 : 相当于张大爷在自己的自留地加了声明,谁需要就来摘

c 复制代码
using namespce N;
int main()
{
    printf("%d\n", N::a);
    printf("%d\n", b);
    Add(10, 20);
    return 0;    
}

3.其它部分

  • 不同域中可以存在同名的变量, 编译器默认会使用局部变量, 但是如果想使用全局变量时, 不写空间名称直接写作用域限定符即表示访问全局变量
c 复制代码
int rand = 10;
int main()
{
	int rand = 20;
	printf("%d\n", ::rand);
	return 0;
}
  • 使用using namespace固然好用, 但不宜过度用, 不同命名空间中有相同的名称, 全部展开, 编译器会报错,例如
bash 复制代码
namespace N1
{
	int a = 10;
	int b = 1;
	int Add(int left, int right)
	{
		return left + right;
	}

	struct Node
	{
		struct Node* next;
		int val;
	};
}
namespace N2
{
	int a = 10;
	int b = 1;
	int Add(int left, int right)
	{
		return left + right;
	}

	struct Node
	{
		struct Node* next;
		int val;
	};
}
using namespace N1;
using namespace N2;

三. C++输入&输出

C++的输入输出需要包含头文件 < isotream > , 直接使用cout(console out 从控制台输出) 和 endl(end line 换行) 会发生报错, 这是因为官方为避免与我们自己定义的名称和库重复, 所以将库的函数名称包含在了std命名空间里面, 所以我们如果使用需要按照命名空间的使用方法使用std

c 复制代码
#include<iostream>

int main()
{
	cout << "Hello World!!!" << endl;
	return 0;
}

正确用法:

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

//或者

#include<iostream>
// std是C++标准库的命名空间名,C++将标准库的定义实现都放到这个命名空间中
using std::cout;
using std::endl;
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++的输入输出可以自动识别变量类型。
  5. 实际上cout和cin分别是ostream和istream类型的对象,>>和<<也涉及运算符重载等知识,这些知识我们我们后续才会学习,所以我们这里只是简单学习他们的使用。后面我们还有有一个章节更深入的学习IO流用法及原理。

注意:早期标准库将所有功能在全局域中实现,声明在.h后缀的头文件中,使用时只需包含对应头文件即可,后来将其实现在std命名空间下,为了和C头文件区分,也为了正确使用命名空间,规定C++头文件不带.h;旧编译器(vc 6.0)中还支持<iostream.h>格式,后续编译器已不支持,因此推荐使用+std的方式。

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语言的用法,这些又用得不是很多,我们这里就不展开学习了。后续如果有需要,我们再配合文档学习

std命名空间的使用惯例:

std是C++标准库的命名空间,如何展开std使用更合理呢?

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

四. 缺省参数

1. 缺省参数的概念

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

c 复制代码
void Func(int a = 0)
{
	cout << a << endl;
}

int main()
{
	Func();// 没有传参时,使用参数的默认值
	Func(10);// 传参时,使用指定的实参
	return 0;
}

2.缺省参数的分类

  • 全缺省参数
c 复制代码
 void Func(int a = 10, int b = 20, int c = 30)
 {
     cout<<"a = "<<a<<endl;
     cout<<"b = "<<b<<endl;
     cout<<"c = "<<c<<endl;
 }
  • 半缺省参数
c 复制代码
 void Func(int a, int b = 10, int c = 20)
 {
     cout<<"a = "<<a<<endl;
     cout<<"b = "<<b<<endl;
     cout<<"c = "<<c<<endl;
 }

传值时只能顺序传,不能跳着传

c 复制代码
int main()
{
	F2(1, 2, 3);
	F2(1, 2);
	F2(1);
	F2();

	// 必须顺序传,不能跳跃着传
	// F2(1, ,2);

注意:

  1. 半缺省参数必须从右往左依次来给出,不能间隔着给
  2. 缺省参数不能在函数声明和定义中同时出现, 如果缺省参数的声明和定义分离, 以声明的值为主.
c 复制代码
  //a.h
  void Func(int a = 10);
  
  // a.cpp
  void Func(int a = 20)
 {}
  
  // 注意:注意:如果生命与定义位置同时出现,以声明为主
  1. 缺省值必须是常量或者全局变量
  2. C语言不支持(编译器不支持)

五. 函数重载

自然语言中,一个词可以有多重含义,人们可以通过上下文来判断该词真实的含义,即该词被重载了。

比如:以前有一个笑话,国有两个体育项目大家根本不用看,也不用担心。一个是乒乓球,一个是男足。前者是"谁也赢不了!",后者是"谁也赢不了!"

1.函数重载的概念

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

即同一作用域, 参数个数不同, 参数类型不同, 类型顺序不同, 三种情况构造重载关系

c 复制代码
#include<iostream>
using namespace std;

//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 << "void f(int a)" << endl;
}

//3.参数类型顺序不同

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

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

int main()
{
	Add(10, 20);
	Add(10.1, 20.2);

	f();
	f(10);

	f(10, 'a');
	f('a', 10);
	return 0;
}

不同作用域, 可以同名
同一作用域, 可以同名, 满足重载规则

c 复制代码
 //不同作用域 可以同名
namespace N1
{
	void Swap(int* pa, int* pb)
	{
		cout << "void Swap(int* pa, int* pb)" << endl;
	}
}

namespace N2
{
	void Swap(int* px, int* py)
	{
		cout << "void Swap(int* pa, int* pb)" << endl;
	}
}


// 同一作用域 可以同名,满足重载规则
void Swap(double* pa, double* pb)
{
	cout << "void Swap(double* pa, double* pb)" << endl;
}

using namespace N1;
using namespace N2;
// 他们两依旧是ok,不是重载关系

int main()
{
	int a = 0, b = 1;
	double c = 0.1, d = 1.1;
	// 调用歧义
	//Swap(&a, &b);

	Swap(&c, &d);

	return 0;
}

这段代码N1, N2 不在同一作用域, 可以同名, 不构成重载, 但是命名空间全部展开, 会引发调用歧义.

下面两个构造函数重载

f()但是这样调用存在歧义

c 复制代码
void f()
{
	cout << "f()" << endl;
}

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

2. 为什么C++支持函数重载, 而C语言不支持重载呢?

这是因为在函数定义与声明分离时, 编译阶段无法直接获得函数地址, 只是通过函数声明,语法可以通过, 链接阶段, C语言直接通过函数名字进行查找, 不支持函数重载, 而C++是使用修饰后的函数名字去查找就可以支持函数重载.

具体过程如下:

  1. 一个项目工程中, 声明和定义通常是分离的, 声明在.h文件中, 定义在.cpp文件中, 如下图

    代码在变成可执行程序需要经过编译和链接, 编译分为三个过程, 首先经过预处理, 将.h与.cpp文件进行结合, 进行头文件展开, 宏替换, 条件编译等等, 生成.i文件, 此时就没有.h文件的事情了. 然后, 通过编译, 检查语法, 生成汇编代码, 此时语法通过, 检查语法匹配, 但没有函数地址. 最后通过汇编转成二进制机械码, 但是有函数的定义, 才能生成一堆汇编指令, 所以在链接阶段, 需要查找函数地址, 也就是将Test.o和Stack.o链接在一起.

如图所示, 转成汇编代码, 在调用StackInit()函数时, 语法通过, 然后执行下一句, jmp指令会通过StackInit()函数名去查找函数地址

函数有一对要执行的指令, 函数地址就是第一句指令的地址.

那么,也就是说在声明与定义分离时, 链接过程中需要通过函数名字去找函数地址, 而C语言直接使用函数名字查找,不支持重载, 而C++使用过修饰后的名字查找, 支持重载.

如下图, 我们先将函数定义注释, 就可以看到, 发上了链接错误, 此时我们再看如何进行修饰的, Windows平台下, C++会根据参数列表(参数个数 或 类型 或 类型顺序) 进行函数名字的修饰, 函数重载, 修饰出来的名字不一样, 那就可以区分. 而C语言则没有对函数名字进行修饰

不同从操作系统对函数名字的修饰规则也不太一样, 比如在Linux操作系统下,对这三个函数的修饰就很直观, 以首字母进行修饰

而我们将文件改成.c 就可以发现C语言没有对函数名字修饰, 所以不支持重载

对比Linux会发现,windows下vs编译器对函数名字修饰规则相对复杂难懂,但道理都是类似的,我们就不做细致的研究了。

六. 引用

1.引用的概念

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

比如:李逵,在家称为"铁牛",江湖上人称"黑旋风"。

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

c 复制代码
void TestRef()
{
	int a = 10;
	int& ra = a; //定义引用类型
	printf("%p\n", &a);
	printf("%p\n", &ra);

}
int main()
{
	TestRef();

	return 0;
}


注意: 引用类型必须和引用实体是同种类型的

2.引用的特性

  1. 引用在定义时必须初始化
  2. 一个变量可以有多个引用
  3. 引用一旦引用一个实体, 再不引用其它实体
c 复制代码
void TestRef()
{
    int a = 10;
    int& ra = a;//<====定义引用类型
    printf("%p\n", &a);
    printf("%p\n", &ra);
}

3.常引用

c 复制代码
void TestConstRef()
{
    const int a = 10;
    //int& ra = a;   // 该语句编译时会出错,a为常量
    const int& ra = a;
    // int& b = 10; // 该语句编译时会出错,b为常量
    const int& b = 10;
    double d = 12.34;
    //int& rd = d; // 该语句编译时会出错,类型不同
    const int& rd = d;
}

4.引用场景

  1. 做参数

之前我们在另一个函数交换值需要传地址, 有了引用就可以直接传引用,此时left就相当于a的别名, 而right就相当于b的别名.

c 复制代码
void Swap(int& left, int& right)
{
   int temp = left;
   left = right;
   right = temp;
}

int main()
{
	int a = 10;
	int b = 20;
	Swap(a,b);
	return 0;
}
  1. 做返回值

后面我们在细讲

5.引用和指针的区别

c 复制代码
typedef struct ListNode
{
	int val;
	struct ListNode* next;
}LTNode;

//之前我们不带哨兵位的链表插入结点需要传递二级指针
void ListPushBack(LTNode** phead, int x);

//这里也可以传递phead指针的引用
void ListPushBack(LTNode*& phead, int x);

//有的教材还可能这样写
typedef struct ListNode
{
	int val;
	struct ListNode* next;
}LTNode, * PNode;
//其本质也是一样的,只是将struct ListNode* 类型重命名为PNode
void ListPushBack(PNode& phead, int x);

这段代码, 是将z赋值给y, 因为引用一旦引用实体,就不可以引用别的实体

c 复制代码
int main()
{
	// int& a;

	int x = 0;
	int& y = x;
	int z = 1;
	// y变成z的别名呢?
	// 还是z赋值给y
	y = z;

	return 0;
}

不管是引用普通变量还是指针变量, 只能权限的缩小, 而不能权限的放大 ,指针赋值也是, 因为被const修饰的地址不可以修改

c 复制代码
int main(){

	int x = 0;
	int& y = x; //权限的平移,可以
	const int& z = x;//权限的缩小,可以
	y++;//可以,z++不可以




	const int m = 0;
	// int& n = m;m是只读的,n变成我的别名,n的权限是可读可写,权限的放大,不可以

	const int& n = m; //可以,不是权限的放大

	int p = m;// m拷贝的给p,p的修改不影响m

	// 权限的放大
	// p1可以修改 *p1不可以,const修饰是的*p1
	const int* p1 = &m;
	// p1++;
	// int* p2 = p1;//不可以,p1的内容不可以修改
	const int* p2 = p1;//可以,常量赋值给常量

	// 权限的缩小,可以
	int* p3 = &x;
	const int* p4 = p3;

	return 0;
}

在语法概念上引用就是一个别名,没有独立空间,和其引用实体共用同一块空间。

在底层实现上实际是有空间的,因为引用是按照指针方式来实现的。

引用和指针的不同点:

  1. 引用概念上定义一个变量的别名,指针存储一个变量地址。
  2. 引用在定义时必须初始化,指针没有要求
  3. 引用在初始化时引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何一个同类型实体
  4. 没有NULL引用,但有NULL指针
  5. 在sizeof中含义不同:引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32位平台下占4个字节)
  6. 引用自加即引用的实体增加1,指针自加即指针向后偏移一个类型的大小
    int main()
    {
    int a = 10;
    int& ra = a;
    cout<<"&a = "<<&a<<endl;
    cout<<"&ra = "<<&ra<<endl;
    return 0;
    }
    int main()
    {
    int a = 10;
    int& ra = a;
    ra = 20;
    int* pa = &a;
    *pa = 20;
    return 0;
    }
  7. 有多级指针,但是没有多级引用
  8. 访问实体方式不同,指针需要显式解引用,引用编译器自己处理
  9. 引用比指针使用起来相对更安全

感谢点赞, 关注, 收藏!!!

相关推荐
码农君莫笑9 分钟前
Blazor项目中使用EF读写 SQLite 数据库
linux·数据库·sqlite·c#·.netcore·人机交互·visual studio
云空11 分钟前
《解锁 Python 数据挖掘的奥秘》
开发语言·python·数据挖掘
青莳吖21 分钟前
Java通过Map实现与SQL中的group by相同的逻辑
java·开发语言·sql
Buleall28 分钟前
期末考学C
java·开发语言
重生之绝世牛码30 分钟前
Java设计模式 —— 【结构型模式】外观模式详解
java·大数据·开发语言·设计模式·设计原则·外观模式
小蜗牛慢慢爬行36 分钟前
有关异步场景的 10 大 Spring Boot 面试问题
java·开发语言·网络·spring boot·后端·spring·面试
Algorithm15761 小时前
云原生相关的 Go 语言工程师技术路线(含博客网址导航)
开发语言·云原生·golang
shinelord明1 小时前
【再谈设计模式】享元模式~对象共享的优化妙手
开发语言·数据结构·算法·设计模式·软件工程
Monly211 小时前
Java(若依):修改Tomcat的版本
java·开发语言·tomcat
boligongzhu1 小时前
DALSA工业相机SDK二次开发(图像采集及保存)C#版
开发语言·c#·dalsa