简述C++

  • C语言是面向过程的一门编程语言,“将要解决的问题分解成一个个小问题,依次解决”
  • C+ +是对C语言的扩展和增强 : 面向对象、通用算法(泛型编程)

待更

标准库

C++ 中包含了C标准库的移植版本,C标准库的头文件xxx.h 基本上 变成了cxxx

stdio.h在C+ +中对应的是cstdiomath.h变成 了cmathstring.h变成 了cstring

当然,并不是所有头文件都是如此,如malloc.h仍然不变。


## 条件编译

在C语言中,我们学过 条件编译,使用方法如下:

1
2
3
4
5
#if 条件表达式
程序段1
#else
程序段2
#endif

功能为:如果#if后的条件表达式为真,则程序段 1 被选中,否则程序段 2 被选中。

注意,必须使用 #endif 结束该条件编译指令。

当然不止有以上的使用,还有将是否进行了某宏定义作为触发条件的条件编译。

条件编译指令说明
#if如果条件为真,则执行相应操作
#elif如果前面条件为假,而该条件为真,则执行相应操作
#else如果前面条件均为假,则执行相应操作
#endif结束相应的条件编译指令
#ifdef如果该宏已定义,则执行相应操作
#ifndef如果该宏没有定义,则执行相应操作

条件编译在编写头文件的时候可以防止重复引用造成的程序错误问题,现已成为编写头文件时墨守成规的格式了。

1
2
3
4
5
6
7
8
9
10
11
12
#ifndef HEADERFILENAME_H
#define HEADERFILENAME_H
/*


头文件内容


注:HEADERFILENAME即是头文件的文件名
(宏定义时注意用大写)
*/
#endif

## 名字空间

名字空间 可以类比于存放变量名的空间。某个变量在当前名字空间下所代表的含义可以与其他名字空间的变量不同。

例如不同班级都可能有同名的学生,如“张伟”,为防止对象名冲突,因此需要使用空间限定

  • 计科1701:: 张伟
  • 机械1 803:: 张伟

名字空间除了系统定义的名字空间之外,还可以自己定义,定义名字空间用关键字namespace,使用名字空间时用符号::对其指定。

  1. 不指定名字空间的变量或函数都是当前名字空间下的变量或函数。
  2. 不定义名字空间的情况下,属于全局名字空间。
  3. 同一个名字空间可以定义多次

使用:

  • 名字空间::名字:每次需要使用该名字空间下的对象时,在main()内部通过此代码代替对象明本身。

  • using 名字空间::名字:在开头进行声明,代表此后的对象名字为名字的对象都代表的是名字空间里的那个变量

  • using namespace std:在开头声明引入名字空间中所有的名字,此后就不再重复对某个变量进行限定了,类似于java/python中的import的功能

下面将以C++的输入输出流为例,更好的理解名字空间。


C++的输入输出

除了C标准库的stdio.h中提供的函数scanf()printf()外,C++自己也有一种输入输出的方式

输入与输出

头文件#include<iostream>

  • cout是一个标准输出流变量(对象),代表控制台窗口

  • cin是一个标准输入流变量(对象)

  • endl是标准换行符对象,等效于字符中的\n

    以上三者均需要通过名字空间进行限定才能使用

  • std就是 一个名字空间,而cout就是名字空间std内部的一个(对象)名字

    使用时需要加上名字空间限定std::cout(如果之前没有声明的话)

  • <<除了作为左移运算符之外,在C++中可作为输出运算符。

    如语句:cout << x 中,x是一个数据,该语句可以实现“打印”x的数据。

  • >>除了作为右移运算符之外,在C++中可作为输入运算符。

    如语句:cin >> x 中,x是一个变量,该语句可以实现“输入”并对x赋值。

~coutcin可以看出其实就是c-plus-output和c-plus-input即输出与输入的缩写~


举例

1
2
3
4
5
6
7
8
9
10
11
12
#include<iostream> //C++标准输出流头文件

int main(){

int x = 100;

std::cout << x; //使用std内的cout对x进行输出
std::cout << "finish\n"; //使用std内的cout对字符串进行输出
std::cout << "A New Row" << std::endl;// 使用std内的endl完成换行的输出

return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include<iostream> //C++标准输出流头文件

using std::cout;//声明cout在std名字空间内,注意有分号

int main(){

int x = 100;

//使用std内的cout对x进行输出
cout << x;//这里直接使用cout即可
cout << "finish" << std::endl;//想要用endl实现换行功能,还得使用空间限定

return 0;
}
1
2
3
4
5
6
7
8
9
#include<iostream> //C++标准输出流头文件
using namespace std; //声明使用std名字空间内所有的名字,注意有分号
int main(){

int x = 100;
cout << "哈哈哈!" << endl;//使用std内的cout和endl,且不再进行限定了
cout << "嘿嘿!";
return 0;
}

从上述例子中,我们领会到了名字空间的使用,也不难发现,输出流可以串起来使用!

如:cout << "finish" << std::endl;

输入的使用如下,输入同样也能够实现连续输入:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include<iostream> //头文件

using namespace std;
int main(){

int x ;
int a[3];
cin >> x; //注意,此处是两个大于符号
cout << "您刚刚输入的值是:" << x << endl;

cin >> a[0] >> a[1] >> a[2]; //中间输入时默认要输入空格隔开
cout << "sum = " << a[0]+a[1]+a[2];
return 0;
}

不仅如此,C++还支持有对文件的输入输出……

初看·文件输入输出

此处涉及到之后要学习的 类 和 方法 的知识,可选择性查看

之后还会再次详细介绍

头文件#include<fstream>

  • 类:ofstream输出流对象,以此来实现对文件的输出(类似于c的fprintf
  • 类:ifstream输入流对象,以此来实现对文件的输入(类似于c的fscanf
  • 方法:xxx.close()通过此方法实现对文件的关闭(类似于c的fclose()

举例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include<fstream> //文件输入输出头文件
#include<iostream>
#include<string>//字符串头文件

using namespace std;

int main(){


ofstream oF("myfile.txt");//以输出的方式 打开/新建 指定路径下的文件
int x = 999;
oF << "input some data:" << x << endl;
oF.close();

ifstream iF("myfile.txt");//以输入的方式 打开 指定路径下的文件
string str1,str2,str3; //str此时是一个string类对象
iF >> str1 >> str2 >> str3;
iF.close();
cout << str1 << " " << str2 << " " << str3 << endl;
return 0;
}

引用变量

引用变量是其他变量的别名。如同一个人的外号或小名。
既然是引用, 定义引用变量时就必须指明其引用的变量主体。而且定义之后不可更改。如:

1
2
3
int a = 3,b = 4;
int &r = a; //&符号 这里引用变量相当于: 可以用 r 来表示 a
int &r = b; //报错

其本质类似于C中的指针:

1
2
3
int a = 3;
int *p = &a;
//此时的 *p 就类似于前面的 r 一样,都可以通过对自身的改变从而改变a的值

我们知道,在C语言中,想要使用一个函数来更换a,b的值,不使用指针的方式是无法达到实际更改的。

具体实现方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include<iostream>
using namespace std;
void swap(int *a,int *b){
//通过指针进行交换
int t;
t = *a;
*a = *b;
*b = t;
}

int main(){
int a = 3,b = 4;
cout << "a =" << a << ",b =" << b << endl;
swap(&a,&b);//将a,b的地址传给指针
cout << "a =" << a << ",b =" << b << endl;
return 0;
}

而有了C++的“引用”之后,我们能够得到一种新的解决方案:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include<iostream>
using namespace std;
void swap(int &x,int &y){
//通过&符号,将a,b用x,y进行引用,此时x就相当于a,y就相当于b
int t;
t = x;
x = y;
y = t;
}

int main(){
int a = 3,b = 4;
cout << "a =" << a << ",b =" << b << endl;
swap(a,b);
cout << "a =" << a << ",b =" << b << endl;
return 0;
}

默认参数&函数重载

默认形参

在真正的标准C语言中,是不支持“默认形参”的

而且,定义变量必须在真正意义上的语句之前。

如:printf("你好!\n");int a = 0;不被允许的!

此外,for(int i=0;i<5;i++)即在for循环处直接定义变量也是错误的!

但是在之后更为人性化的C标准修订之后,有些语法是可以通过的,可以说C和C++大致上共通了,所以默认形参在C中也被广泛使用。

关于在C中使用默认形参的方法:https://blog.csdn.net/pipisorry/article/details/25437893

默认参数指的是当函数调用时,可以省略传递实参而自动使用的一个默认值。例如,将void wow(int n)设置成n有默认值为1,则函数调用wow()相当于wow(1)。这极大地提高了使用函数的灵活性


==默认形参必须在非默认形参右边,即一律靠右==

即 形如 void fn(int x=0,int d)的函数定义方法是错误的。


函数重载

==C+ +允许同一作用域里有同名的函数,只要它们的形参不同。==

函数重载是根据 函数签名 实现的,而函数签名则是由函数名与函数形参共同决定,与返回值无关.例如:

1
2
int add(int x,int y);
double add (double x,double y);
  • 上述两种函数并不会发生冲突
1
2
int add(int x,int y);
double add(int x,int y);
  • 上述两种函数发生冲突
1
2
int add(int a,int b,int c);
int add(int a,int b);
  • 上述两种函数不会发生冲突

函数模板

在上述例子中,我们给出了两个add()函数,可以方便计算intdouble型的两数之和,但是需要编写两个重复性的代码,不便于后期修改与处理。

因此,我们可以通过函数模板来实现高效的函数声明与定义:

1
2
3
4
template <typename T>
T add(T x,T y){
return x+y;
}

实现了传入类型为T的值,并返回类型为T的返回值。

使用函数时,通过 模板实例化调用函数:

1
2
3
cout << add<int>(3,5) << endl;
cout << add<double>(3.6,5.7) << endl;
cout << add<string>("hello","world") << endl;

用尖括号框住数据类型,则T就代表该数据类型,此方法类似于:

1
2
3
4
typedef int T;
T add(T x,T y){
return x+y;
}

但是,上面的这个T是不可灵活更改的。


事实上,模板拥有自动推断的功能,在上述例子中,我们调用函数时甚至可以不加尖括号,直接调用:

1
2
3
4
5
6
cout << add(3,5) << endl;
cout << add(3.6,5.7) << endl;

//但是有歧义性的参数就不可这样省略性使用,如:
cout << add(3,5.7) << endl;
//3 是int型,而5.7是double型

动态内存分配

在C中,我们曾经使用malloc函数对变量进行动态内存的分配,而C++中也可以进行这样的操作。

不仅如此,C++还提供了一种更加便利的方法:

1
2
3
4
5
6
7
8
int main(){

int *p = new int; //代替int *p = (int *)malloc(sizeof(int));
*p = 9;
delete p; //代替free(p);

return 0;
}

事实上,这是极其简单,甚至有BUG的写法,比较完备的写法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
int main(){

//申请内存
int *p = new int;

//判断
if(p == NULL)
return;//因为可能会申请内存失败,需判断是否成功,保证完备性

//操作
*p = 9;

//释放
if(p != NULL){
delete p;
p = NULL;
}

return 0;
}

二维数组的动态分配


String & Vector

在之前的各种示例中,我们多次使用到string这个“变量类型”。其实,这是一个C++中系统内部的class类。(类与C中的结构体类似,之后会有详细解释)

string基本使用

与C中的字符与字符数组组成的字符串有异同,string实例可以通过调用 方法、成员 实现许多功能。所需头文件<string>

  1. 赋值/初始化

    1
    2
    string str = "hello world";
    string str2("hello minecraft");
    • 以上两种方式都可以创建一个string实例
  2. 部分方法

    1
    2
    3
    4
    s.size(); //返回s这个字符串的大小(int)
    s.substr(a,b); //返回s这个实例对象的第a到b截到的新字符串,类型也是string
    s.find("xxx"); //返回int型下标,即s中出现xxx字符串的第一个下标position
    s.insert(pos,"xxx"); //在s中下标为pos的地方插入字符串"xxx"
  3. 运算

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
      s = s1 + s2; 
    /*
    将s1与s2拼接在一起得到的新string对象赋给s
    (此处+号的逻辑是我们之后需要了解的 运算符重载)
    */

    s[4] = 'H';
    /* 下标运算符
    与C一样,string对象同样也是字符数组组成的字符串
    也可以通过下标确切地更改字符串单个字符的内容
    */

vector基本使用

vector,向量,类似于C/C++中的数组,但是其长度可以动态增长,所需头文件<vector>

又与上面的string(类)有些许不同,vector是一个类模板。如,使用vector<int>就能实例化一个int型的vector类(向量)。

  1. 赋值/初始化

    1
    2
    vector<int> v; //int型空向量
    vector<double> v2 = {1.1,2.2,3.3}; //double型默认起始长度是3的向量
  2. 方法与成员

    1
    2
    3
    4
    v.push_back(x); //在v向量末尾添加一个数据x
    v.pop_back(); //删除最后一个数据
    v.size(); //返回向量此时的长度
    v.resize(n); //从第一个起,将v向量截断使size为n

面向对象

在前面的学习中,我们多次提到了 “类”、“对象”等词汇。接下来我们将真正进入面向对象的世界~~

创建类与对象

一般在开发中,常常把类的创建独立出来用头文件.h来 “保存” 我们“造的轮子”

通过关键词:class + 自定义的类名 进行类的创建

快速开始

基础示例

1
2
3
4
5
6
7
class ClassName{
int arr1;
float arr2;
char arr3;
void f(){};
int g(int i,int j,bool flag=flase){return x};
}

这与C语言的结构体类似,不同的是,类可以将函数也作为内部成员,这种在类里面声明/定义的函数我们一般称之为类的方法。

使用

1
2
3
4
5
6
7
int main(void){
ClassName c;
c.arr1 = 1;
c.f();
c.g(1,2);//默认形参使flag=flase
return 0;
}

与C语言的结构体类似,直接通过类名+.+成员调用.

访问运算符

通过访问控制符一定程度上可以保证程序的安全性,灵活使用可以使得类的外部不能轻易使用 私有属性和方法

示例:

1
2
3
4
5
6
7
8
class People
{
public:
int age;
int weight;
private:
string name;//名字作为私有成员,不对外开放
};

类的访问控制符有四种:

  • public

    可以通过外部访问方式访问类内部的public成员

  • private

    不能通过外部访问方式访问类内部的private成员

  • protected

    不可以通过外包访问方式访问类内部的protected成员

  • 默认【即不加任何修饰符default

在一个类的内部,所有的成员可以相互访问,访问控制符是透明的;访问控制符是针对外部而言的

外部访问包括两种方式:

  • 通过类名访问类内部的成员
  • 通过类对象名访问类内部成员

==更多访问控制符的使用我们将在继承与包的学习中再次提及==


规范书写

一般,我们将函数方法作为public成员,而变量成员作为private成员。下面给出C++类创捷的规范书写

1
2
3
4
5
6
7
8
9
10
11
class ClassName
{
private:
int arr1;
char arr2;
string arr3;
public:
ClassName();
~ClassName();
void function1(int,char,string);
};

这时候就会产生疑问了。规范书写的ClassName()~ClassName()是什么?

如果是变量成员为什么有括号?如果是函数为什么没有返回值?而且为什么名字和类名一模一样?

这就是 构造函数和析构函数!


构造函数

malloc不会使用构造函数

析构函数

参数化表

参数化表

1
2
3
CBox(int l=10,int w=20,int h=30):l(l),
w(w),
h(h)

this指针

抽象、封装与信息隐藏

普通常量

常量const):共用数据的保护

使得数据能够在一定范围内共享,又保证数据不被任意修改

常对象

  • 功能:常对象数据成员的值是常量,不能修改

  • 格式

    • 类名 const 对象名[(实参表)]
    • const 类名 对象名[(实参表)]
  • 说明

1)常对象必须进行初始化,而且不能被更新;

2)常对象不能调用该常对象的非const型的成员函数;除非把要调用的成员函数定义为常成员函数const.

【避免非const型的成员函数修改常对象中的数据成员的值,因为const型的成员函数不能修改对象中的数据成员的值】

  • 举例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#include <iostream>
using namespace std;
class Time{
public:
Time(int,int,int);
int hour;
int minute;
int sec;
void get_time( );
};
Time::Time(int h,int m,int s){
hour=h;
minute=m;
sec=s;
}
void Time::get_time( ) {cout<<hour<<":"<<minute<<":" <<sec<<endl;}

/*
void main(){
Time t1(10,15,36);
t1.get_time( );
t1.hour=20; //可以修改对象中数据成员的值
t1.get_time( );
}
*/

/*
void main(){
const Time t1(10,15,36); //利用const定义常对象
t1.get_time( ); //常对象不能调用该常对象的非const型的成员函数;除非把要调用的成员函数定义为常成员函数const
t1.hour=20;//以上均error
}
*/

常函数

前面的示例代码中,我们提到:

常对象不能调用该常对象的非const型的成员函数;除非把要调用的成员函数定义为常成员函数const

也就是说,函数也是可以定义为常函数的。

  • 格式

  • <返回值类型> <函数名> (<参数表>) const {代码块}

  • 注意

    常成员函数不能调用另外一个非cosnt成员函数


有时在编程时有要求,一定要修改常对象成员中的某个数据成员的值(例如类中有一个用于计数的变量count,其值应当不能变化)此时可把该数据成员声明为mutable,如:
mutable int count; : 定义一个在常对象中可以被改变的数据成员
count声明为可变的数据成员,这样就可以用声明为const的成员函数来修改它的值。

  • 举例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Student{
public:
Student(int n,float s):num(n),score(s){}//语句0
void change(int n,float s) const{num=n;score=s;}//语句1
void display() const{cout<<num<<"\t"<<score<<endl;}// 语句2
private:
mutable int num;//语句3
mutable float score;//语句4
};

int main(){
Student const stud(101,78.5);//语句5
stud.display();
stud.change(101,80.5);
stud.display();
return 0;
}
  1. 语句0:利用参数化表的形式编写构造函数,这样可以对const的对象进行初始化
  2. 语句1&语句2:将成员函数设置为const型函数常量,这样可以通过const的对象调用
  3. 语句3&语句4:将成员变量设置为mutable类型,这样可以使得const型函数对其进行修改
  4. 语句5:先定义一个const的类对象,然后对应于语句0可以进行初始化

常成员

  • 格式const <类型> <数据成员变量名>
    例: const int hour;
  • 说明:只能通过构造函数的参数初始化表对常数据成员进行初始化。任何地方都不能使用赋值语句对常数据成员赋值。

在类体中声明了某一个数据成员为常数据成员后,该类所有对象中的该数据成员的值都是不能改变的,但不同对象中该变量成员的值可以是不同的(分别在初始化时指定)

  • 举例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <iostream.h>
class Time{
public:
Time(int,int,int);
const int hour;
int minute;
int sec;
void get_time( ) const;
};
/*因为hour是const,所以不能使用这种方式初始化
Time::Time(int h,int m,int s){
hour=h;
minute=m;
sec=s;
}*/
Time::Time(int h,int m,int s):hour(h),minute(m),sec(s){};
void Time::get_time() const {cout<<hour<<":"<<minute<<":" <<sec<<endl;}
void main(){
Time t1(10,15,36);
// t1.hour=20;//常数据成员的值不能修改
}

总结

作用表

  1. 如果在一个类中,有些数据成员的值允许改变,另一些数据成员的值不允许改变,则可以将一部分数据成员声明为const,以保证其值不被改变,可以用非const的成员函数引用这些数据成员的值,并修改非const数据成员的值。
  2. 如果要求所有的数据成员的值都不允许改变,则可以将所有的数据成员声明为const,或将对象声明为const(常对象),然后用const成员函数引用数据成员,这样起到“双保险”的作用,切实保证了数据成员不被修改。
  3. 不要误认为常对象中的成员函数都是常成员函数,常对象只保证其所有数据成员的值不被修改。
  4. 如果在常对象中的成员函数未加const声明,则编译系统会把它当非const成员函数处理。
  5. 常成员函数不能调用另外一个非cosnt成员函数。

指针常量

  • 定义
    将指向对象的指针变量声明为const型,指针值(地址)不变【编号不变】。(地址指向的内存中的内容可以改变)。
  • 格式
    <类名> * const <指针变量名>;
  • 举例
1
2
3
4
5
6
int a = 1;
int b = 2;
int* const p = &a;
int* q = &b;
*p = 5;//没问题,之后a的值为5
p = q;//报错,p保存的地址不能更改
  • 说明
    • 可以保证指针的值不被改变,维护了安全性

常量指针

  • 格式:定义指向常对象的指针变量的一般形式

    • const <类名>* <指针变量名>;
  • 说明

    1. 如果一个对象已被声明为常对象,只能用指向常对象的指针变量指向它

      而不能用一般的 (指向非const型对象的) 指针变量去指向它

      1
      2
      3
      Time const t(12,45,45);
      Time *p = &t;//报错,不能指向常对象
      const Time *q = &t;//成功
    2. 如果定义了一个指向常对象的指针变量,并使它指向一个非const的对象,则其指向的对象是不能通过指针来改变的

      1
      2
      3
      Time t(12,45,45);
      const Time *p = &t;
      t.ChangeHour(11);//报错,指向t之后,t不能再改变了
    3. 指向常对象的指针最常用于函数的形参,目的是在保护形参指针所指向的对象,使它在函数执行过程中不被修改


常引用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class Time{
public:
Time(int,int,int);
int hour;
int minute;
int sec;
};
Time::Time(int h,int m,int s){ //定义构造函数
hour=h;
minute=m;
sec=s;
}

/******原始版本******/
void fun(Time &t){t.hour=18;}
//形参t是Time类对象的引用

/******优化版本******/
void fun(const Time &t){t.hour=18;}
//形参t是Time类对象的引用
//保证t指向的对象不能被改变,保证安全性

int main(void){
Time t1(10,13,56); // t1是Time类对象
fun(t1); //实参是Time类对象,可以通过引用来修改实参t1的值
cout<<t1.hour<<endl; //输出t1.hour的值为18
return 0;
}

经常用常指针和常引用作函数参数。这样既能保证数据安全,使数据不能被随意修改,在调用函数时又不必建立实参的拷贝。用常指针和常引用作函数参数,可以提高程序运行效率。


静态成员

目的:在同类的多个对象之间实现数据共享,不用全局对象,而用静态的数据成员。(例:学生类中的平均分、最高分等)

静态成员是同一个类中所有对象共享的成员,不是某一对象的成员。

用静态数据成员可以节省内存,是所有对象所公有的,对多个对象来说,静态数据成员只存储一处,供所有对象共用。

静态数据成员的值对每个对象都是一样,它的值是可以更新的。静态数据成员是静态存储的,具有静态生存期。

存储方式

  • 声明:通过关键词static对成员进行声明
    • 如:static float num;
  • 初始化:一般情况下,类的静态成员不能直接在声明的时候赋予初值,要在类外部实现
    • float ClassName::num = 10;
    • <变量类型> <类名>::<变量名> = <初始值>;
    • 即 无需再加static关键词
  • 使用
    • 通过生成的实例对象名引用:<对象名>.<静态成员名>
    • 通过类名引用静态数据:<类名>::<静态成员名>

静态成员函数

目的:使用静态成员函数引用静态数据成员

  • 声明/定义:例子:static void print();

  • 说明

    1. 静态成员函数属于类的静态成员,不是对象成员。对静态成员的引用不需要用对象名

    2. 在静态成员函数的实现中不能直接引用类中说明的非静态成员,可以引用类中说明的静态成员。

      如果静态成员函数中要引用非静态成员时,可通过对象来引用。

      1
      2
      3
      4
      //静态成员函数内部:
      cout<<height<<endl; //height为static,引用本类中的静态成员,合法
      cout<<width<<endl; //width是非静态数据成员,不合法
      cout<<a.width<<endl; //引用本类对象a中的非静态成员
    3. 公有的静态成员函数既可以有通过相应的对象访问,也可以通过其所属的类名来引用。

      1
      2
      Box::print();//合法
      box1.print();//也合法
  • 实例

    1
      

友元函数

类外访问公用成员(public);只有本类中的函数可以访问本类的私有成员(private);
特例:友元(friend)可以访问与其有好友关系的类中的私有成员(private).

  • 定义:
    在类的声明语句中的public部分加入friend <返回类型> <其他地方的函数名>(<参数表>)
    则该函数可以访问类中的私有成员(private)
  • 使用:
    1)将普通函数声明为友元函数

类模板

在前面的学习中,我们给出了C++标准库中的vector的基本使用。如想要创建int型的向量组,仅需通过以下语句定义:

1
vector<int> a={0,1,2,3};

之后我们学习到类之后,我们可以发现,vector就是一个类,我们通过类的实例创建的方式得到了名字为a的对象。

那么,<int>又代表什么呢?这将是接下来要讲的 类模板


运算符重载

以下均以 “复数类模板” 作为例子一一介绍运算符重载极其优势

单目

输入输出流重载

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include<iosream>
using namespace std;
/*
复数类模板
real为实部;
img为虚部.
*/
class Complex{
private:
double m_dReal;
double m_dImg;
public:
Complex();
Complex(double r,double i);
Complex(const Complex &c);//通过引用进行对象拷贝
~Complex();
void Disp();
friend ostream & operator << (ostream &output,Complex &c);
};
/*部分函数实现已省略*/
ostream & operator << (ostream &output,Complex &c){
output << c.m_dReal;
if(c.m_dImg >= 0){
output << '+' << c.m_dImg;
}
output << endl;
}

继承

继承好处

解决软件重用(software reuseablility)问题,可以利用已有的软件资源,节约人力、物力、财力、时间,效率高

定义

继承是使用已存在的类的定义作为基础建立新类的技术.

新类的定义可以增加新的数据或新的功能,也可以用父类的功能,但不能选择性地继承父类。

通过使用继承我们能够非常方便地复用以前的代码,能够大大的提高开发的效率。

(1)子类拥有父类非private的属性和方法。

(2)子类可以拥有自己属性和方法,即子类可以对父类进行扩展

(3)子类可以用自己的方式实现父类的方法。(多态)

继承方式

用于规定派生类中由基类继承到的那部分成员在派生类中的访问控制权限。

继承方式用下述三个关键字之一来指定:public:公有继承;protected:保护继承;private:私有继承。

注:构造函数和析构函数不能被继承

下面给出不同继承方式的差异对比:

继承方式基类中访问权限派生类中访问权限
公有继承publicpublic
protectedprotected
private不可访问
私有继承publicprivate
protectedprivate
private不可访问
保护继承publicprotected
protectedprotected
private不可访问

子类的构造函数

构造函数不能被继承,只能通过调用!!

逻辑:构造函数初始化过程是从父类“向外”扩散的,也就是从父类开始向子类一级一级地完成初始化。

两种情况:

(1)自动调用父类构造函数:父类有默认构造函数

(2)明显的调用父类构造函数:父类没有默认构造函数。C++中要求在子类的构造函数中,使用参数化表的方式,调用父类没有默认构造函数

例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
#include<string>
using namespace std;

class Student{
public:
Student();
Student(int n,string na);
~Student();
void display(void);
protected:
int m_iNum;
string m_strName;
};

//类Graduate用public方式继承自Student
class Graduate:public Student{
protected:
//新增成员
float f_Score;
public:
Graduate();
//原句:Graduate(int n,string na,float s);
Graduate(int n,string na,float s):Student(na,n),f_Score(s){}
~Graduate();
void display(void);
};


Student::Student(){
i_Num = 0;
str_Name = "default";
}
Student::Student(string na,int n){
this->i_Num = n;
this->str_Name = na;
}
void Student::display(void){
cout << this->str_Name << endl;
cout << this->i_Num << endl;
}
Student::~Student(void){}

Graduate::Graduate(){
//子类事实上会在此处自动调用父类的无参构造函数
//即:Student::Student();
f_Score = 0;
}
/* 原句:会报错
Graduate::Graduate(int n,string na,float s){
this->i_Num = n;
this->str_Name = na;
this->f_Score = s;
}*/
Graduate::~Graduate(){}
//重写子类的display函数
void Graduate::display(void){
Student::display();
cout << f_Score << endl;
}

继承时的构造原则:

1)如果子类没有定义构造函数,那子类就是调用父类的无参数的构造函数。

2)如果子类定义了构造函数,那么不管父类有没有定义构造函数,在创建子类对象时,首先会调用父类的无参数的构造函数,然后在调用子类自己的构造函数。

3)在创建一个子类对象时,如果子类没有显示调用父类的构造函数,那么就会首先调用父类的默认的无参的构造函数(其实和上面的2是一样的)。

4)在创建子类对象时,如果此时父类有自己书写了无参的构造函数,此时如果子类没有显示的调用父类的构造函数,那就就会首先调用父类自己书写的的无参的构造函数。

5)在创建子类对象时,如果父类只有有参的构造函数,子类没有显示的调用的父类的构造函数,则会出错。因为如果父类只有有参的构造函数,那么子类就必须显示调用父类有参的构造函数,

6)如果子类显示调用父类的有参构造函数,那么必须使用初始化列表形式进行初始化。

子类对象的内存模型

继承模型

赋值兼容规则

公有继承下的赋值兼容规则

公有派生类的对象可赋给其基类对象,基类对象不能赋给派生类对象的规则,称为赋值兼容规则。

1
2
//已知:

1)派生类对象可以向基类对象赋值。

2)派生类对象可以替代基类对象向基类对象的引用进行赋值或初始化。

3)如果函数的参数是基类对象或基类对象的引用,相应的实参可以用子类对象。

4)派生类对象的地址可以赋给指向基类对象的指针变量,也就是说,指向基类对象的指针变量也可以指向派生类对象

多重继承

多态

虚函数

异常处理

推荐资料

1.《数学之美》

2.C与C++的变量、函数与常量_by刘俊

3.Matrix/Complex封装实例

参考资料

1.C++快速入门|HWDONG-Bilbili

2.C语言默认参数值的实现|CSDN