从C到C++
从C到C++
一、介绍
C++是对C语言的扩展,C语言程序也是C++程序。
C/C++程序只执行叫做main()
的主函数。
1 | int main(){ |
编译器:g++(linux)、clang、 Visual Studio C++ Compiler
1 | g++ -c file.cpp |
二、标准库、注释、条件编译
C++中包含了C标准库的移植版本,C标准库的头文件x.h
基本上变成了cx
。但也有例外,如malloc.h
仍然没变
stdio.h在C+.+中对应的是cstdio、 math. h变成了cmath、 string. h变成了 cstring
注释与C中一致
条件编译与C中一致
1 |
|
三、标准输入输出、命名空间
标准输入输出头文件:#include<iostream>
(派生出的一些其他流:文件流#include<fstream>
)
cout
是一个标准输出流变量(对象),代表控制台窗口,是标准名字空间std的一个名字。必须加上标准名字空间限定std∷
,即std::cout
cin
是一个标准输入流变量(对象),代表键盘,是标准名字空间std的一个名字。必须加上标准名字空间限定std∷
,即std::cin
<<
是一个运算符,假如o是一个输出流对象,X是一个数据,则o<<X
为输出数据X,且该表达式执行的结果为o
1
2 cout << "hello"; //返回cout对象
cout << endl; //返回cout对象上述方式等同于下述方式:
1 cout << "hello" << endl; //都返回cout对象,所以可以直接拼接(从左至右结合)
>>
也是一个运算符,假如o是一个输入流对象,X是一个变量,则o>>X
为输入数据并赋值给X,且该表达式执行的结果为o
名词空间就是为了防止名词冲突,可以通过using
引入:
using std::cout
:单独引出coutusing namespace std
:引入整个名字空间
疑问:cin、cout等不已经在头文件iostream中了么,为何还要包含标准命名空间std?似乎是#include没用?又好像cin、cout等一批东西同时存在于这两个“分立”的东西里,该如何解释?
确实标准的输入输出同时存在于头文件和命名空间中,但头文件和命名空间并不是“分立”的,而是在头文件中包含了标准的命名空间std。
应该这么理解:头文件iostream中有这么几行定义了标准命名空间std的代码
1
2
3
4
5
6
7 namespace std
{
...
cin...
cout...
...
}当我们没有
using namespace std
或者使用std::cin
、std::cout
的时候,cin,cout是不可见的,也就是说即使我们包含了iostream,但由于没有使用标准命名空间std,使得该命名空间内所有的内容我们无法使用。这就说明了头文件iostream和标准命名空间std的关系——这俩是两个不同的东西,但是由于它们的“包含”关系(在头文件中定义了命名空间std),我们不能说它们是完全独立的。
要另外强调的是iostream和iostream.h是不同的,后者是为了向前兼顾c语言的产物,在新版本的VS中已经删去。iostream.h可以理解为包含了命名空间std的iostream。
四、引用变量、引用形参
引用变量:即其他变量的别名(如同一个人的外号或小名),有以下几点需要注意:
- 定义时必须指明其引用的是哪个变量(作为函数形参除外,实际上实参会赋值给形参)
- 一旦定义就不能再引用其他变量
- 引用变量和被引用的变量类型必须匹配
- 对引用变量的操作就是对它引用的变量的操作
1 | int a = 3; |
作用:C函数的形参都是值参数,形参作为函数的局部变量有自己单独的内存块,当函数调用时,实参将值拷贝(赋值给)形参。对形参的修改不会影响实参。
C代码解决(利用指针)
1 | void swap(int *x, int *y){ |
C++代码解决(利用引用变量)
1 | void swap(int &x, int &y){ |
五、函数的默认形参、函数重载
默认形参:
- 函数的形参可以有默认值
- 默认形参必须在非默认形参右边,即一律靠右
函数重载:
- C++允许作用域里有同名的函数,只要它们的形参不同(类型或个数)
- 函数名和形参列表构造了函数的签名
- 函数重载不能根据返回类型区分函数
六、函数模板
模板是泛型编程的基础,泛型编程即以一种独立于任何特定类型的方式编写代码。
模板是创建泛型类或函数的蓝图或公式。库容器,比如迭代器和算法,都是泛型编程的例子,它们都使用了模板的概念。
每个容器都有一个单一的定义,比如 向量,我们可以定义许多不同类型的向量,比如 vector <int> 或 vector <string>。
-
用
template
关键字增加一个模板头,将数据类型变成类型模板参数1
2
3
4template<typename T>
T add(T x,T y){
return x+y;
} -
给模板参数传递实际的模板参数
1
2
3cout << add<int>(5, 3) << endl;// 上述 T 即 int
cout << add<double>(5.3, 7.8) << endl;// 上述 T 即 double
cout << add(5.3, 7.8) << endl; //自动推断 -
模板自动推断:模板会根据参数列表自动推断出模板类型
七、用户类型定义string、vector
string:
-
是一个用户定义类型,表示的是字符串
1
string s = "hello",s2("world");
- 新版c++中可以用
[]
代替()
- 新版c++中可以用
-
用成员访问运算符
.
访问string类的成员1
2
3cout << s.size() << endl;
string s3 = s.substr(1, 3);
cout << s3 << endl; //"ell" -
可以使用运算符对string对象进行运算,如
+
、[]
1
2
3
4
5
6string s4 = s + " " + s2;
cout << s4 << endl; //"hello world"
s4[0] = 'H';
s4[6] = 'X';
cout << s4 << endl; //"Hello Xorld"
vector:
- 向量,类似于C中的数组,但是可以动态增长,头文件
<vector>
- 实质上是一个类模板,用于实例化产生一个类,如
vector<int>
产生一个数据元素是int的vector<int>
类(向量)- 可以通过
vector<int>
类对象去访问其成员(如成员函数)
- 可以通过
- 可以用运算符进行一些运算
1 |
|
八、指针、动态内存分配
约定:指针就是地址,变量的指针就是变量的地址。可以用取地址运算符&
获得一个变量的地址。
- 指针变量就是存储指针(地址)的变量
- 通过取内容运算符
*
可以得到一个指针变量指向的变量
动态内存分配:
- 用关键字
new
申请堆中的内存(与C中的malloc类似,但是new还会对对象进行初始化(调用构造函数)) - 使用关键字
delete
释放new
申请的内存
1 | //申请int |
九、类和对象
过程式编程:变量(对象)就是一些存储数据的內存块,而过程(函数)对这些数据进行处理
面向对象编程:程序是由不同种类的许多对象相互协作完成的。对象之间通过发送/接收消息来协作完成各种任务。由这些对象构成的程序也称为"对象式系统"
9.1 用户类型定义
- 程序员定义自己的“用户定义类型”如类类型,来表示各种应用问题中的各种概念
- C++标准库已经提供了很多实用的“用户定义类型”,是C++标准库的程序员实现的
- cout是一个ostream类的对象(变量),cin是一个istream的对象(变量)。可以向它们发送消息来输出和输入
- string是一个表示字符串的类。向一个string对象发送一个size()消息,查询该对象包含的字符数目
- 用户类型具体包括:
- 属性
- 操作函数(方法)
- 不同属性或操作的访问权限
9.2 类
基础知识:
-
用
struct
或class
关键字定义一个类。定义的类就是一个数据类型1
2
3
4struct student{
string name;
double score;
} -
类类型的变量通常称为对象
1
student stu;//对象就是类的一个实例
-
可以通过成员访问运算符
.
访问成员
对象数组:可以定义类类型的数组,储存一组类对象
1 | student students[3]; |
类类型的指针变量:T是一个类类型,则T*
就是T指针类型(如int*是int指针类型)。T*
变量可以指向一个类对象
1 | student students[3]; |
- 取内容运算法
*
- 间接访问运算符
->
类的成员函数:
-
类体内定义成员函数:
1
2
3
4
5struct student{
string name;
double score;
void print(){cout<<name<<" "<<score<<'\n';}
}; -
类体内定义成员函数:
1
2
3
4
5
6
7
8struct student{
string name;
double score;
void print();//函数声明
};
void student::print(){//函数实现
cout<<name<<" "<<score<<'\n';
}- 外部函数需要加上类作用域
十、this指针、访问控制、构造函数
10.1 this指针
C++到C的翻译
C++ 是在C语言的基础上发展而来的,第一个 C++ 的编译器实际上是将 C++ 程序翻译成C语言程序,然后再用C语言编译器进行编译。
C语言没有类的概念,只有结构,函数都是全局函数,没有成员函数。翻译时,将 class 翻译成 struct、对象翻译成结构变量是显而易见的,但是对类的成员函数应该如何翻译?对myCar.Modify();
这样通过一个对象调用成员函数的语句,又该如何翻译呢?
C语言中只有全局函数,因此成员函数只能被翻译成全局函数;myCar.Modify();
这样的语句也只能被翻译成普通的调用全局函数的语句。那如何让翻译后的 Modify 全局函数还能作用在 myCar 这个结构变量上呢?答案就是引入“this 指针”。下面来看一段 C++ 程序到C 程序的翻译。
1 | class CCar |
可以看出,类被翻译成结构体,对象被翻译成结构变量,成员函数被翻译成全局函数。但是C程序的全局函数 SetPrice 比 C++ 的成员函数 SelPrice 多了一个参数,就是struct CCar *this
。car.SetPrice(20000);
被翻译成SetPrice(&car, 20000);
,后者在执行时,this 形参指向的正是 car 这个变量,因而达到了 SetPrice 函数作用在 car 变量上的效果。
思考题:以上翻译还不完整,因为构造函数的作用没有体现出来。思考构造函数应该如何翻译。另外,静态成员函数和静态成员变量应如何翻译?
this指针的作用
实际上,现在的C编译器从本质上来说也是按上面的方法来处理成员函数和对成员函数的调用的,即非静态成员函数实际上的形参个数比程序员写的多一个。多出来的参数就是所谓的“this指针”。这个“this指针”指向了成员函数作用的对象,在成员函数执行的过程中,正是通过“Ihis指针”才能找到对象所在的地址,因而也就能找到对象的所有非静态成员变量的地址。
下面程序的运行结果能够证明这一点:
1 |
|
程序的输出结果是:
hello
在上面的程序中,p 明明是一个空指针,为何通过它还能正确调用 A 的成员函数 Hello 呢?因为,参考上面 C++ 到C程序的翻译,P->Hello()
实质上应该是Hello(p)
,在翻译后的 Hello 函数中,cout 语句没有用到 this 指针,因此依然可以输出结果。如果 Hello 函数中有对成员变量的访问,则程序就会出错。
C++ 规定,在非静态成员函数内部可以直接使用 this 关键字,this 就代表指向该函数所作用的对象的指针。看下面的例子:
1 |
|
第9行,this指针的类型是Complex*。因为 this 指针就指向函数所作用的对象,所以 this->rear 和 real 是完全等价的。*this
代表函数所作用的对象,因此执行第 16 行,进入 AddOne 函数后,*this
实际上就是 c1。因此的 c2 值会变得和 c1 相同。
因为静态成员函数并不作用于某个对象,所以在其内部不能使用 this 指针;否则,这个 this 指针该指向哪个对象呢?
当你进入一个房子后,你可以看见桌子、椅子、地板等,但是房子你是看不到全貌了。
对于一个类的实例来说,你可以看到它的成员函数、成员变量,但是实例本身呢?this是一个指针,它时时刻刻指向你这个实例本身。
10.2 访问控制
类定义关键字struct
、class
区别:
- struct:类中的成员默认为
public
(公开) - class:类中的成员默认为
private
(私有)
10.3 构造函数
在创建一个类对象时会自动调用称为构造函数的成员函数,由该函数来完成对对象的创建工作
如果类中没有构造函数,C++会采用默认的构造函数(默认构造函数参数列表为空,或参数列表中均带默认值)
构造函数:函数名和类名相同且无返回类型的成员函数
1 |
|
- 定义类数组时,因为不能对每个对象单独传递参数,只能调用默认的构造函数
十一、运算符重载
各种运算符(+、-、*、/、<<、>>、下标运算符[]等)都能够通过重载,定义出自定义类型的相关操作
- 其中,下标运算符
[]
必须被定义为内部函数(类的成员函数) - 输入输出流运算符
<<
、>>
必须被定义为外部函数
1 | //重载<<操作符 |
11.1 友元函数
类的友元函数:在类外部,有权访问类的所有私有(private)成员和保护(protected)成员。尽管友元函数的原型有在类的定义中出现过,但是友元函数并不是成员函数。
友元可以是一个函数,该函数被称为友元函数;友元也可以是一个类,该类被称为友元类,在这种情况下,整个类及其所有成员都是友元。
如果要声明函数为一个类的友元,需要在类定义中该函数原型前使用关键字 friend
,如下所示:
1 | class student{ |
- 友元函数中没有this指针,则参数有三种情况:
- 要访问非static成员时,需要对象做参数
- 要访问static成员或全局变量时,则不需要对象做参数
- 如果做参数的对象是全局对象,则不需要对象做参数.
下标运算符:
1 | class Point{ |
11.2 常函数
void fun() const {}
:常函数
- 构造函数和析构函数不可以是常函数
- 可以使用数据成员,不能进行修改,对函数的功能有更明确的限定
- 常对象只能调用常函数,不能调用普通函数
- 常函数的this指针是
const 类名*
十二、拷贝构造函数、析构函数
自己实现自定义的String类:
1 |
|
-
细节问题:若上述代码中,main函数中只有以下代码:
1
2
3
4
5int main(){
String str,str2("Hello World");
str2[1] = 'E';
cout << str2 << endl; //"HEllo World"
}最后调用operator<<时,传入的参数s会调用String的构造函数,operator<<结束后会调用String的析构函数,此时会释放s.data,从而导致main结束时调用str2的析构函数报错
-
解决方法:采用后一种拷贝构造函数,为每个变量都申请一块动态内存
12.1 拷贝构造函数
拷贝构造函数是一种特殊的构造函数,它在创建对象时,是使用同一类中之前创建的对象来初始化新创建的对象。拷贝构造函数通常用于:
- 通过使用另一个同类型的对象来初始化新创建的对象
- 复制对象把它作为参数传递给函数
- 复制对象,并从函数返回这个对象
如果在类中没有定义拷贝构造函数,编译器会自行定义一个。如果类带有指针变量,并有动态内存分配(指向内存、打开文件、打开网络端口),则它必须有一个拷贝构造函数,且此时默认的拷贝构造函数会直接拷贝对应的动态内存空间的地址,造成新变量和原本变量指针指向的地址一样(共享同一块内存资源)
拷贝构造函数的最常见形式如下:
1 | classname (const classname &obj) { |
12.2 析构函数
类的析构函数是类的一种特殊的成员函数,它会在每次删除所创建的对象时执行。
析构函数的名称与类的名称是完全相同的,只是在前面加了个波浪号(~
)作为前缀,它不会返回任何值,也不能带有任何参数。析构函数有助于在跳出程序(比如关闭文件、释放内存等)前释放资源。
1 | ~classname () { |
注意:析构函数的调用过程和构造函数的调用过程正好相反
十三、类模板
类似定义函数模板一样,也可以定义类模板。泛型类声明的一般形式如下所示:
1 | template <class type> class class-name { |
在这里,type 是占位符类型名称,可以在类被实例化的时候进行指定。可以使用一个逗号分隔的列表来定义多个泛型数据类型。
下面的实例定义了类 Stack<>,并实现了泛型方法来对元素进行入栈出栈操作:
1 |
|
结果:
1 | 7 |