【C++】初始C++(1)

目录

0 引言 

1 C++关键字(C++98)

2 命名空间

2.1 命名空间的定义

2.2 命名空间的使用(授权) 

2.2.1 直接使用 

2.2.2 部分展开(授权)

2.2.3 全部展开(授权)

3 C++的输入&输出

4 缺省参数

4.1 缺省参数概念

4.2 缺省参数分类

5 函数重载

6 引用

6.1 引用概念

6.2 引用特性

6.3 引用使用场景

6.3.1 做参数

6.3.2 做返回值

6.4 引用和指针的区别


0 引言 

C++是在C语言基础上增加进了面向对象的编程思想,从而拓展升级的一门强大的编程语言。

C语言是结构化和模块化的语言,适合处理较小规模的程序。对于复杂的问题,规模较大的程序,需要高度的抽象和建模时,C语言则不合适。为了解决软件危机, 20世纪80年代, 计算机界提出了OOP(object oriented programming:面向对象)思想,支持面向对象的程序设计语言应运而生。

1982年,Bjarne Stroustrup博士在C语言的基础上引入并扩充了面向对象的概念,发明了一种新的程序语言。为了表达该语言与C语言的渊源关系,命名为C++。因此:C++是基于C语言而产生的,它既可以进行C语言的过程化程序设计,又可以进行以抽象数据类型为特点的基于对象的程序设计,还可以进行面向对象的程序设计。正是因此背景,C++对于C语言的语法是全面兼容的,C语言的一切语法在C++中都适用。

C++既可以进行C语言的过程化程序设计,又可以进行以抽象数据类型为特点的基于对象的程序设计,还可以进行以继承和多态为特点的面向对象的程序设计。C++擅长面向对象程序设计的同时,还可以进行基于过程的程序设计。 C++几乎可以创建任何类型的程序:游戏、设备驱动程序、云、桌面、嵌入式和移动应用等。 甚至用于其他编程语言的库和编译器也使用C++编写。

1 C++关键字(C++98)

相较于C语言的32个关键字,C++总共有63个关键字。

下述是C++关键字一览

asm do if return try continue
auto double inline short typedef for
bool dynamic_cast int signed typeid public
break else long sizeof typename throw
case enum mutable static union wchar_t
catch explicit namespace static_cast unsigned default
char export new struct using friend
class extern operator switch virtual register
const false private template void true
const_cast float protected this volatile while
delete goto reinterpret_cast

2 命名空间

先来看如下C语言程序:

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

这段代码实不能够被运行通过的,编译器会直接报错: error C2365: “rand”: 重定义;这是由于stdlib.h中有一个叫rand的函数,而又定义了一个全局变量rand,这就会导致重定义的问题,C语言是不能解决这种问题的。

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

2.1 命名空间的定义

定义命名空间,需要使用到namespace关键字,后面跟命名空间的名字,名字可以按照自己的喜好来取,然后接一对{}即可,{}中即为命名空间的成员。命名空间中可以定义变量、类型、函数,同时命名空间支持嵌套定义。
如:

namespace mj
{
	//命名空间中可以定义变量/函数/类型
	int r = 10;

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

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

	namespace xxx
	{
		int r = 1;
	}
}

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

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

2.2 命名空间的使用(授权) 

命名空间可以看做是一个封闭的空间,它的权限很高,如果不对其中的成员进行展开(授权)就不能正常使用。

此处就需要引入新的关键字using来帮助我们使用命名空间,这里有三种使用方法:

2.2.1 直接使用 

这里需要引入一个新的运算符:域作用限定符::

按照 命名空间名字::命名空间成员 的方式使用。

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

2.2.2 部分展开(授权)

假如我们只需要使用命名空间中部分成员,那么就可以使用部分展开(授权)的方式。以using 命名空间名字::命名空间成员名的方式。

以上文的mj为例,使用方法如下:

//部分展开(授权)
using mj::Add;
using mj::r;

int main()
{
	printf("%d\n",r);
	printf("%d\n", Add(1, 2));
	return 0;
}

2.2.3 全部展开(授权)

全部展开是通过using关键字直接展开命名空间内所有成员,以using namespace 命名空间名字 的方式展开,如: 

using namespace mj;

3 C++的输入&输出

C++是有很多标准库的,就像C语言的输入输出函数包含在stdio.h一样,C++的标准输入输出流是iostream。

#include <iostream>

//std是c++标准库的命名空间
using namespace std;
int main()
{
	int i;
	double j;
	//>>流提取运算符
	cin >> i >> j;

	//<<流插入运算符
	cout << i << ' ';
	cout << j << 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>格式,后续编译器已不支持,因
此推荐使用<iostream>+std的方式。

std是C++标准库的命名空间,对于std的展开是由规范的:

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

4 缺省参数

4.1 缺省参数概念

C++是支持缺省参数的,这也是C++与C语言的区别之一。

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

如以下程序,输出结果分别是0和10

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

上述程序可以看出,缺省参数其实就是在函数定义时为形参赋一个初始默认值。如果在函数调用时没有为该参数传参,那么就使用默认值。

4.2 缺省参数分类

缺省参数分为全缺省和半缺省两类。 

  • 全缺省参数
    void Func(int a = 10, int b = 20, int c = 30)
    {
        cout<<"a = "<<a<<endl;
        cout<<"b = "<<b<<endl;
        cout<<"c = "<<c<<endl;
    }
    int main()
    {
    	Func();
    
    	// 显示传参,从左往右显示传参
    	Func(1);
    	Func(1,2);
    	Func(1, 2, 3);
    
    	return 0;
    }

    全缺省参数就是函数中的所有参数都是缺省参数,此时传参规则是从左往右依次传参。没有传参的以默认值处理。

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

    此处需要特别注意:

    1. 半缺省参数必须从右往左依次来给出,不能间隔着给;

    2. 缺省参数不能在函数声明和定义中同时出现,这是因为如果生命与定义位置同时出现,恰巧两个位置提供的值不同,那编译器就无法确定到底该用那个缺省值。

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

5 函数重载

函数重载是函数的一种特殊情况,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 << "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;
}

运行结果:

注意:如果函数形参个数相同,类型相同,顺序也相同,但是函数返回值类型不同,是不能构成函数重载的。比如以下程序:

int f(double a, int b)
{
    cout << "int f(double , int b)" << endl;
    return 0;
}
void f(double a, int b)
{
    cout << "void f(double , int b)" << endl;
}

假设我们调用f(1.1 , 1 ),此时编译器不知道会调用上述两个函数中的哪一个,会造成歧义。所以这种情况是不能构成函数重载的。

6 引用

6.1 引用概念

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

就比如说,一个人可以有多个代号。我们可以用名字来代表一个人,也可以用其他的外号来指代一个人。无论是用姓名还是用外号,指代的都是同一个人。也就是说,C++中的引用,就是对于同一个变量有多个名字,所有的名字指向的都是同一个东西。

引用的格式是:类型& 引用变量名(对象名) = 引用实体;

void TestRef()
{
    int a = 10;
    int& ra = a;//<====定义引用类型
    printf("%p\n", &a);
    printf("%p\n", &ra);
}

上述程序就是引用的一个简单实例,ra和a是等价的,他们都代表着内存中的同一块空间。注意:引用类型必须和引用实体是同种类型的

6.2 引用特性

引用有三个重要的特性:

  1. 引用在定义时必须初始化,否则编译器会报错;
  2. 一个变量可以有多个引用;
  3. 引用一旦引用一个实体,就不能再引用其他实体。

6.3 引用使用场景

引用一般有两种常用的使用场景:做参数和做返回值。

6.3.1 做参数

引用作为参数使用时,需要在函数参数声明类型。

一般情况下,如果函数需要用到指针的地方,都可以改为使用引用,会比较方便。

以经典的交换函数为例,下面是C语言版本:

void Swap(int* left, int* right)
{
    int temp = *left;
    *left = *right;
    *right = temp;
}

下面是C++版本: 

void Swap(int& left, int& right)
{
    int temp = left;
    left = right;
    right = temp;
}

由于引用变量就是原变量的别名,因此对于引用变量的修改就等价于对原变量的修改。对于不支持引用的C语言,实现两个变量的交换就需要传地址调用,本质上也是一种传值传参;而C++的方式叫做传引用传参,大大增加了代码的可读性,方便了设计者。

传引用传参的效率是高于传值传参的。于是可以总结出传引用传参的两个优点:

  1. 提高效率;
  2. 应对输出型参数时更加方便(形参修改影响实参)

6.3.2 做返回值

我们先来看如下程序:

int Add(int a, int b)
{
    int c = a + b;
    return c;
}
int main()
{
    int ret = Add(1, 2);
    return 0;
}

对于这个程序,函数Add应该返回3,但是这里的返回并不是直接将c返回给ret,因为出函数作用域之后,函数栈帧会被销毁,即c指向的空间会被回首。此时c指向的对象是未知的。因此,编译器是创建一个临时变量,将c的值复制给这个临时变量,再将临时变量的值赋值给ret。这就是传值返回的正确逻辑。

传引用返回代码如下:

int& Add(int a, int b)
{
    int c = a + b;
    return c;
}
int main()
{
    int ret = Add(1, 2);
    return 0;
}

传引用返回是这样的:在原函数类型的基础上,将类型改写为引用类型。如上述代码,此时函数最后返回的并不是c,而是c的别名。

不过这种写法是有问题的,以上述代码为例,函数栈帧销毁后,即使我们得到了c的别名并将其赋值给ret,可以通过ret找到c指代的空间。但是在栈帧销毁以后,c原本指代的空间已经被回收,此时在通过ret去访问就会形成类似野指针的情况。因此这种情况下使用传引用返回是不恰当的,应当使用传值返回。

不过,假如我们可以确保出了函数作用域之后原对象并没有被影响,就可以使用传引用传参来提高效率。我们以顺序表的修改为例:

C语言版:

struct SeqList
{
	int a[10];
	int size;
};
 /*C的接口设计
 读取第i个位置的值*/
int SLAT(struct SeqList* ps, int i)
{
	assert(i < ps->size);
	// ...
	return ps->a[i];
}
 //修改第i个位置的值
void SLModify(struct SeqList* ps, int i, int x)
{
	assert(i < ps->size);

	// ...
	ps->a[i] = x;
}
int main()
{
	struct SeqList s;
	s.size = 3;
	// ...
	SLModify(&s, 0, 10);
	SLModify(&s, 1, 20);
	SLModify(&s, 2, 30);
	cout << SLAT(&s, 0) << endl;
	cout << SLAT(&s, 1) << endl;
	cout << SLAT(&s, 2) << endl;

	return 0;
}

C++版:

int& SLAT(struct SeqList& ps, int i)
{
	assert(i < ps.size);
	// ...
	return (ps.a[i]);
}

int main()
{
	struct SeqList s;
	s.size = 3;
	// ...
	SLAT(s, 0) = 10;
	SLAT(s, 1) = 20;
	SLAT(s, 2) = 30;
	cout << SLAT(s, 0) << endl;
	cout << SLAT(s, 1) << endl;
	cout << SLAT(s, 2) << endl;

	return 0;
}

因为对于顺序表,其数组空间是在全局定义的,出了函数作用域之后也不会被修改,于是就可以使用传引用返回的方式,直接对返回对象进行修改,这样就可以省略一个Modify函数的设计。

综上可以得出传引用返回的两个优点:

  1. 提高效率;
  2. 可以直接修改返回对象

同时,传引用返回也有一个缺陷,那就是只有出了函数作用域原对象还在(即占用的内存空间未被修改)才可以使用,否则会造成类似野指针一样不安全的情形。

6.4 引用和指针的区别

引用和指针有如下几个不同点:

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

猜你喜欢

转载自blog.csdn.net/fbzhl/article/details/131893785