从C过渡到C++需要了解的“新特性”
阅读原文时间:2023年07月08日阅读:3
#include <iostream>
using namespace std; //编译指令

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

头文件

在C语言的传统中,头文件使用扩展名.h

//C风格
#include <stdio.h>

老式的C++延续了这个传统

//老式C++
#include <iostream.h>

而新式的C++抛弃了这个传统,转而去掉了.h

//新式C++
#include <iostream>

C++将部分C头文件转换为C++头文件并重新命名:去掉.h并加上前缀c

#include <cstdio>

在代码中包含cstdio,C++程序中也可以出现scanf()printf()

对于纯粹的C++头文件(比如iostream)来说,去掉.h不只是形式上的变化,还可能包含了命名空间

风格

约定

示例

C

C++

命名空间

C

.h结尾

stdio.h

×

老式C++

.h结尾

iostream.h

×

×

新式C++

没有扩展名

iostream

×

转换后的C

加上前缀c,没有扩展名

cmath

×

由于C使用扩展名.h来表示这是一个C头文件

如果C++头文件继续沿用这个拓展名,容易造成混淆

其中一种解决方案是另起一个扩展名,比如.hpp等。

但最后大家却一致同意不使用任何扩展名 可能是懒的选了

编译指令

如果你使用的头文件是iostream,而不是iostream.h

那么你应该使用下面的语句来使iostream中的定义可用

using namespace std;

这句话告诉编译器,使用std这个命名空间,这样可以更方便的使用cout

关于命名空间的详细内容,留在了文章末尾的拓展部分。对于刚刚开始转向C++的萌新来说,最简单的方法就是先记住要写这句话就完了。

cout

cout << "Hello World!" << endl;

什么是cout

cout是一个预定义的对象(还没有提对象的概念,暂且把它当成一个东西、一个物件)。cout知道如何在屏幕上显示字符串、数字、单个字符等。

cout << 56;
cout << ' ';
cout << 9.10;

这个<<又是什么?

<<是C位运算的左移操作符,用来操作位。

在C++有了新的用途——输出。箭头方向指明了信息流动的方向。

信息可以流向cout从屏幕上输出,也可以通过键盘输入流向某个变量

cin >> a;

箭头方向指向a,说明这个信息是流向了a

endl是一个控制符,相当于C里面的换行符\n

C++融合了3种不同的编程方式

  • 面向过程

(Procedure Oriented Programming)

  • 面向对象

(Object Oriented Programming)

  • 泛型编程

(Generic Programming)

通过C语言的学习,相信你已经对面向过程有所了解。假如我们现在要用程序来模拟一次上课的场景,用面向过程的思维来设计的话就是:

  • if (时间 == 8:00) 上课
  • 老师讲课,学生听课
  • 进入循环
  • ……
  • if (时间 == 9:40) 下课

那如果我们要用面向对象的思维来模拟上课场景,该怎么做?

什么是对象

面向对象在台湾有另一种叫法——物件导向。所谓的对象其实就是我们常说的东西,对象可以是

  • 一个按钮
  • 一盏灯

一个对象由下面两样东西组成

  • 数据(Data)
  • 操作(Operations)


以一盏灯为例

  • Properties可以是灯丝,Status是灯是否开启、灯的颜色。
  • Operations可以是开关,这个操作会改变内部的Data

上面这副蛋图,蛋黄位置的DataOperations所包裹。想要碰到Data只能穿过Operaions

为什么要把Data包裹起来?举个栗子,电视机的外壳。这个外壳除了让产品更好看之外,更重要的是保护着电视内部的电气元件,通常外壳上都会贴一个警告标签

非专业人士请勿拆卸

显然厂家不希望用户拆开这个外壳,如果用户拆开了外壳,自己去拔插电视内部的线路,很有可能会损坏电视。

但这个外壳会影响我们的使用吗?当然不会,设计者留出了很多按钮开关来操作电视机,这些按钮开关就是我们说的Operations

在OOP的世界里,大致有两类程序员

  • 设计类的程序员(设计者)
  • 使用类的程序员(用户)

对于设计者来说,用户不能直接碰到内部数据。用户只能通过设计者给出的接口来访问内部数据。

对于用户来说,用户关心的是功能而不是原理。正如电视机的按钮,按下就可以启动,用户不需要了解其中的电子电路知识。设计者对用户屏蔽了具体实现细节。

把数据和数据的操作放在一起,形成有机联系,叫做封装。

所以,如果我们要用面向对象思维来模拟一个上课过程,我们要做的就是写对象

  • 一个老师对象
  • 若干个学生对象

这些对象的互动就是上课的过程。

从对象到类

  • class

a collection of things sharing a common attribute.

  • object

entity

object是实体,class是概念

小明是一个学生对象,他的名字是 小明,期末考成绩90

从小明身上抽象出学生的一些共有属性,可以得到下面的类

class Student{
    int sid;
    int score;
};

另外有一个学生对象小红,她也会有学生类里面的属性

所以,class定义了object该长什么样,而object里面有具体的属性。

OOP的五项原则

  1. Everything is an object.
  2. A program is a bunch of objects telling each other what to do by sending messages.
    程序是由一堆互相之间传递消息的对象组成的。与之对应的是,面向过程语言(例如C)设计出来的程序是由一堆函数组成的。
  3. Each object has its own memory made up of other objects.
    每一个对象有他自己的内存,这些对象里面还可以有对象。
  4. Every object has a type.
    每个对象有一个自己的类型。
  5. All objects of a particular type can receive the same messages.

一个特定类型的对象可以接受相同的信息。反过来也可以说,能接受相同消息的对象可以认为是一样的对象。

//这是一个C结构体的标签
struct Student{
    int sid;
    int score;
};

用C语言定义这样一个结构体要在前面加struct

//C风格的定义
struct Student zs;

用C++定义时可以省略struct关键字

//C++风格的定义
Student zs;

同样的事情在C语言中实现要用到typedef

typedef struct _Student{
    int sid;
    int score;
}Student;

C++的结构体标签中还可以定义函数,这在C语言是不被允许的行为

//C++ 包含了操作方法的结构体
struct Student{
    int sid;
    int score;
    void haha( void ){
        cout << "haha..." << endl;
    }
};

结合前面提到的OOP原理
C++的结构体可以放操作方法,有数据又有操作方法,所以C++中的结构体也是一个类class

struct Student{
    int sid;
    int score;
    // 操作方法
    void haha( void ){
        cout << "haha..." << endl;
    }
};

上面的struct可以改写成下面的class

class Student{
public:
    int sid;
    int score;
    void haha( void ){
        cout << "haha..." << endl;
    }
};

访问控制

public

如果声明类的某个成员为public,那么这个成员可以被类外面的语句、函数随便使用。以Student类为例,其全部成员声明为public,可以直接修改zs的数据

Student zs;
zs.sid = 007;

private

改写Student

class Student{
private:
    int sid;
    int score;
public:
    void haha( void ){
        cout << "haha..." << endl;
    }
};

现在sidscore是这个类所私有的成员,此时不允许在外部用zs.sid = 007来修改内部数据,必须是类里面的成员才能修改它们。

class Student{
private:
    int sid;
    int score;
public:
    void haha( void ){
        cout << "haha..." << endl;
    }
    void get_sid( void ){    //修改sid的接口
        cin >> sid;
    }
};

如果省略了publicprivate关键字

  • struct默认全部成员公开
  • class默认全部成员私有

在C99之前,C语言是没有表示真假的布尔值,统统用int

#define TRUE  1
#define FALSE 0


int flag = 5 > 3;

flag的值只有01两种情况

C语言把非0当作真,0当作假,在C++也是一样的

//测试用例
if (3) {
    cout << 1 << endl;
}

if (-2) {
    cout << 2 << endl;
}


//屏幕输出
1
2

C++提供了bool型变量,bool只占用一个字节的空间,用truefalse这两个字面值常量来表示真假。

bool flag = true;
flag = false;

int类型的数据来给bool类型赋值时

  • 非0的认为是true

  • 0认为是false

    bool flag = 123;
    cout << flag << endl;

    //屏幕输出
    1

C++是一门强类型的语言,声明变量的时候必须清楚地知道表达式的类型,然而要做到这一点并非容易。
auto关键字让编译器自己去推断类型。和int这些特定类型不同,auto定义的变量必须有初始化。

auto a = 3;
a /= 2;
cout << a << endl;


//屏幕输出
1

3来给a初始化,编译器推断aint

auto a = 3.0;
a /= 2;
cout << a << endl;
cout << sizeof(a) << endl;


//屏幕输出
1.5
8

3.0a初始化,编译器推断出adouble类型。

引用是已经定义的变量的别名

int b = 1;
int &r = b;

声明引用变量时必须进行初始化定义引用时,引用一旦创建好之后引用来源不能改变。

引用示例

C函数的参数传递,要么是值传递,要么是指针传递。

//C版本的交换函数
void swaq( int *a, int *b ) {
    int temp;
    temp = *a;
    *a = *b;
    *b = temp;
}

C++的参数传递不仅仅是值,还可以是引用。所以可以把交换函数的参数改为引用。

void swaq(int& a, int& b) {
    int temp;
    temp = a;
    a = b;
    b = temp;
}

在C代码中。看到func(a)可以放心的肯定func()得到的是一个值的拷贝,a原本的值不会被修改。

而到了C++,参数传递还可以传引用,看到func(a)不能简单地下结论。

如果你不希望参数引用的变量被修改,应该使用const

void func(const int& a);

右值引用

使用下面的语句给一个右值创建引用

const int& r = 123;
int&& r = 123;    //右值引用

简要了解即可

C++可以编译下面的代码

(a = 3) = 666;


//屏幕输出
666
  • 在C语言中,(a = 3)的值为33是一个常量,常量不能做左值

  • 在C++,(a = 3)a的引用,可以继续赋值。

    int a[5] = { 1,2,5,3,4 };
    for (int i : a) {
    cout << i << ' ';
    }

    //屏幕输出
    1 2 5 3 4

依次打印出数组a的5个元素

加上&表示引用后,可以修改元素的值

for (int& i : a) {
    i++;
}

for (int i : a) {
    cout << i;
}


//屏幕输出
2 3 6 4 5

申请内存

malloc()接受一个数值,指明要申请几个字节

//C版本
int size = 10;
int* p;
p = (int*)malloc(sizeof(int) * size);

到了C++,程序员不用自己数数了,直接指明我要什么,程序自己计算要多少空间。

//C++版本
p = new int[size];

释放内存

//C版本
free(p);


//C++版本
delete[] p;

new了之后一定要记得delete,有借有还再借不难。

加上 [] 释放整个数组,而不是只释放p[0]

函数重载

C++可以编译下面的代码

int add( int x, int y );
int add( int x, int y, int z );

有两个函数名相同但参数列表不同的函数,在编译时编译器就会选取符合参数列表的函数。

错误重载1:相同的原型

但下面的代码不能通过编译

int add( int x, int y );
int add( int y, int z );

虽然参数的名字不相同,但两者的原型是一样的,都是

int add( int, int );

无法重载。

错误重载2:重载返回类型

下面的代码也不能通过编译

int add( int x, int y );
void add( int x, int y );

仅仅是返回类型不一样,编译器无法判断究竟重载哪个函数。
无法重载。

错误重载3:有歧义的重载

int add(int x, int y) {
    return x + y;
}

double add(double x, double y) {
    return x + y;
}

int main(void) {
    cout << add(1, 2.5) << endl;
    return 0;
}

main函数里调用add的语句有歧义。1可以转换为double,2.5也可以转换为int,编译器不知道该重载哪个。

操作符重载

现在有一个向量Vector

struct Vector {
    int x;
    int y;
};

向量的加法

(

x

1

,

y

1

)

+

(

x

2

,

y

2

)

=

(

x

1

+

x

2

,

y

1

+

y

2

)

(x_1,y_1)+(x_2,y_2) = (x_1+x_2,y_1+y_2)

(x1​,y1​)+(x2​,y2​)=(x1​+x2​,y1​+y2​)
+号只能对整数浮点数求值,通过重载运算符+,用+就可以实现向量加法

Vector operator +(const Vector& a, const Vector& b) {
    Vector c;
    c.x = a.x + b.x;
    c.y = a.y + b.y;
    return c;
}


int main() {
    Vector a, b;
    a.x = 3;
    a.y = 4;
    b.x = 1;
    b.y = 5;
    Vector sum;
    sum = a + b;
    cout << sum.x << " " << sum.y << endl;
    return 0;
}


auto f = []( int a, int b )-> int{ return a + b; };

这是函数的一种写法

  • f是一个函数指针
  • ( int a, int b )是参数列表
  • {}前面写函数的返回类型
  • {}里面写函数的内容。

命名空间可以帮助我们避免不经意的名字定义冲突,以及使用库中相同名字导致的冲突。标准库定义的所有名字都在命名空间std中。
通过命名空间使用标准库,当使用标准库中的一个名字时,必须使用::显式说明。

std::cout << "Hello World!" << std::endl;

要使用命名空间,头文件iostream不能有.h
using编译指令使得std命名空间里的所有名称都可用

之后都不需要使用std::