C++基础知识总结(一)
C++对C语言的提高
命令空间简单使用
引用命令空间的三种方式:
直接指定标识符。例如
std::cout<<"hello"<<std::endl;
;
使用using
关键字。例如using std::cout;
;导入整个命令空间。例如
using namespace std;
导入std
命名空间;1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//定义一个命令空间
namespace space1{
int a = 100;
int b = 200;
}
//using namespace std;
//只导入其中的cout和endl
using std::cout;
using std::endl;
int main(int argc, char const **argv)
{
cout<<"hello"<<endl;
using namespace space1;//直接导入命令空间
cout<<a<<" "<<b<<endl;
return 0;
}const
关键字的加强C
语言中的const
是一个冒牌货,即使用来修饰变量的值不能修改,但是还是可以通过指针指向这个变量,然后修改指针指向的内存来修改这个变量的值;注意
const
和#define
的区别: ①#define
只是在预编译时进行字符置换,例如#define PI 3.14
,这里的PI
不是变量,没有类型,不占用存储单元。②而const float PI = 3. 14
定义了常变量PI
,它具有变量的属性,有数据类型,占用存储单元,只是在程序运行期间变量的值是固定的,不能改变(真正的不能改变);
测试:
1 |
|
输出(可以看到a
没有改变(C
语言中会变)):
1 | a = 100, *p = 200 |
引用-重点
- 变量名,本身是一段内存的引用,即别名(
alias
). 引用可以看作一个已定义变量的别名; - 对变量声明一个引用,并不另外开辟内存单元,例如
int &b = a
,则b
和a
都代表同一个内存单元(使用sizeof()
测a
、b
大小是相同的)。引用与被引用的变量有相同的地址; - 在声明一个引用时,必须同时初始化(和常量有点类似),即声明它代表哪一个变量;
- 当声明一个变量的引用后,改引用一直与其代表的变量相联系,不能再作为其他变量的别名。
&
符号前有数据类型时,是引用。其他都是代表取地址;- 引用所占用的大小跟指针是相等的,引用可能是一个”常指针”(
int *const p
); - 对引用的初始化,可以是一个变量名,也可以是另一个引用。如
int a = 3; int &b = a; int &c = b;
此时,整形变量a
有两个别名b
、c
; - 不能建立
void
类型的引用(但是有void *
类型指针(万能指针))。不能建立引用的数组(可以有指针数组);
使用经典的swap
问题来看引用作为形参的简单使用:
1 |
|
输出:
1 | a = 100, b = 200 |
指针引用
可以建立指针变量的引用,如:
1 | int a = 5; |
下面看一个使用指针引用的例子,对比使用二级指针和使用指针引用的区别:
1 |
|
输出:
1 | age = 13, name = zhangsan |
没有引用指针
- 由于引用不是一种独立的数据类型,所以不能建立指向引用类型的指针变量。
- 但是,可以将变量的引用的地址赋值给一个指针,此时指针指向的是原来的变量。
例如:
1 | int a = 3; |
上面代码和下面一行代码相同:
1 | int *p = &a; |
输出*p
的值,就是b
的值,即a
的值。
不能定义指向引用类型的指针变量,不能写成:
1 | int& *p = &a; //企图定义指向引用类型的指针变量,错误 |
const
引用
- 如果想对一个常量进行引用,必须是一个
const
引用; - 可以对一个变量进行 常引用(此时引用不可修改,但是原变量可以修改)。这个特征一般是用在函数形参修饰上,不希望改变原来的实参的值;
- 可以用常量或表达式对引用进行初始化,但此时必须用
const
作声明(内部是使用一个temp
临时变量转换);
测试:
1 |
|
输出:
1 | b = 20, ref2 = 20 |
默认参数、函数重载、作用域运算符
- 如果全局和局部有相同名字变量,使用
::
运算符来操作全局变量;
默认参数要注意: 如果默认参数出现,那么右边的都必须有默认参数,也就是只有参数列表后面部分的参数才可以提供默认参数值; - 函数重载规则: ①函数名相同。②参数个数不同,参数的类型不同,参数顺序不同,均可构成重载。 ③返回值类型不同则不可以构成重载。
- 一个函数,不能既作重载,又作默认参数的函数。
简单使用:
1 |
|
new
、delete
的使用
C
语言中使用malloc
函数必须指定开辟空间的大小,即malloc(size)
,且malloc
函数只能从用户处知道应开辟空间的大小而不知道数据的类型,因此无法使其返回的指针指向具体的数据类型。其返回值一律为void *
,使用时必须强制转换;C++
中提供了new
和delete
运算符来替代malloc
和free
函数;- 差别:
malloc
不会调用类的构造函数,而new
会调用类的构造函数。②free
不会调用类的析构函数,而delete
会调用类的析构函数;(析构函数释放的是对象内部的内存,而delete
释放的是对象,而delete
也出发析构,所以都可以释放)
例如
1 | new int; //开辟一个存放整数的空间,返回一个指向整形数据的指针 |
简单测试:
1 |
|
输出:
1 | *p1 = 10 |
C++面向对象基础
一个简单案例
题目,判断两个圆是否相交。
1 |
|
输出:
1 | 两圆相交! |
构造函数和析构函数
- 如果用户没有定义默认的,系统会提供一个默认构造函数,但是如果用户已经定义了构造函数,系统就不会提供默认的构造函数;
- 系统也会提供一个默认的拷贝构造函数,默认是浅拷贝,形如
Clazz c1(c2)
的使用,将c1
拷贝给c2
; C++
有一个参数初始化列表的特性,注意不能用在数组上;- 构造函数也可以是带有默认参数;
- 析构函数被调用的情况:① 如果用
new
运算符动态的建立了一个对象,当用delete
运算符释放该对象时,先调用该对象的析构函数。②static
局部对象在函数调用结束之后对象并不是放,因此也不调用析构函数。只有在调用exit
或者main
函数结束的时候才会调用static
的析构函数。 - 构造函数和析构函数的顺序: ①先构造的后析构,后构造的先析构;
拷贝构造函数
1 |
|
构造函数中的参数初始化列表:
- 注意构造对象成员的 顺序跟初始化列表的顺序无关;
- 而是跟成员对象的定义顺序有关;输出:
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
class Box{
public:
Box(); //默认的无参
//参数初始化表
Box(int l, int w, int h):l(l),w(w),h(h)
{
strcpy(this->name, "zhangsan"); //默认
}
Box(int l, int w, int h, const char *name):l(l),w(w),h(h)
{
strcpy(this->name,name); //字符串不能那样初始化
}
int volume();
private:
char name[10];
int l,w,h;
};
Box::Box()
{
l = 10;
w = 10;
h = 10;
strcpy(this->name, "zhangsan");
}
int Box::volume()
{
return l*w*h;
}
int main(int argc, char const **argv)
{
Box b1;
printf("%d\n", b1.volume());
Box b2(20, 20, 20);
printf("%d\n", b2.volume());
Box b3(30, 30, 30, "lisi");
printf("%d\n", b3.volume());
return 0;
}
1 | 1000 |
深拷贝和浅拷贝
- 深拷贝是有必要的,因为析构函数释放内存的时候,如果使用浅拷贝拷贝了一个对象,释放两个对象指向得到同一个内存两次,就会产生错误。
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
class Teacher{
public:
Teacher(int id, const char* name){
this->id = id;
int len = strlen(name);
this->name = (char*)malloc(len+1);
strcpy(this->name, name);
}
//显式的提供一个拷贝构造函数,完成深拷贝动作
// 防止在析构函数中 释放堆区空间两次
Teacher(const Teacher &thr){
id = thr.id;
//深拷贝
int len = strlen(thr.name);
name = (char*)malloc(len + 1);
strcpy(name, thr.name);
}
//必须要显式的提供深拷贝构造函数,不然会释放两次
~Teacher(){
if(name != NULL){
free(name);
name = NULL;
}
}
void print(){
printf("id: %d, name = %s\n", id, name);
}
private:
int id;
char *name;
};
int main(int argc, char const **argv)
{
Teacher t1(1, "zhangsan");
t1.print();
Teacher t2(t1);
t2.print();
return 0;
}
指向对象成员函数的指针
- 注意: 定义指向对象成员函数的指针变量的方法和定义指向普通函数的指针变量不同,定义指向成员函数的指针变量的方法:
void (Time:: *p)();
(对比普通的:void (*p)();
) : 定义p
为指向Time
类中公共成员函数的指针变量; - 可以使用上面的
p
指针指向一个公用成员函数,只需把公用成员函数的入口地址赋给一个公用成员函数的指针即可,如p = &Time::get_time
;
简单测试:
1 |
|
输出:
1 | 10:10:30 |
常对象
- 可以在定义对象时加上关键字
const
,指定对象为常对象。常对象必须要有初值,凡是希望保护数据成员不被改变的对象,都可以声明为常对象。 - 基本语法:
类名 const 对象名 [(实参表)]
或者const 类名 对象名 [(实参表)]
,举例:Time const t(10, 10, 30)
和const Time t(10, 10, 30)
; - 如果一个对象被声明为常对象,则通过改对象只能调用它的常成员函数,不能调用对象的普通成员函数;例如
void get_time() const
(常成员函数); - 常成员函数可以访问常对象中的数据成员,但不允许修改常对象中数据成员的值;
常对象成员-常数据成员&常成员函数
①常数据成员
- 只能通过构造函数的参数初始化表对常数据成员进行初始化,任何其他函数都不能对常数据赋值;
- 因为常数据成员不能被赋值;
②常成员函数
- 常成员函数声明: 例如
void get_time() const
,常成员函数只能引用本类中的数据成员,不能修改它们; - 注意: 如果定义了一个常对象,只能调用其中的
const
成员函数,不能调用非const
成员函数(不论这些函数是否会修改对象中的数据); - 常成员函数不能调用另一个非
const
成员函数;
数据成员 | 非const 成员函数 |
const 成员函数 |
---|---|---|
非const 数据成员 |
可以引用,可以改变值 | 可以引用,不可改变值 |
const 数据成员 |
可以引用,不可改变值 | 可以引用,不可改变值 |
const 对象 |
不允许 | 可以引用,不可改变值 |
指向对象的常指针
- 基本语法
Time * const p = &t1
和基本的常指针差不多; - 只能指向同一个对象不能改变指针的指向;
- 意义是作为函数的形参,不允许在函数执行过程中改变指针变量,使其始终指向原来的对象;
指向常变量、对象的指针
①指向常变量的指针 - 如果一个变量已经被声明为常变量,只能用指向常变量的指针变量指向它,而不能用非
const
指针指向它;
1 | const char c[] = "boy"; |
const
指针除了可以指向const
变量,还可以指向非const
变量,此时不能通过指针来改变变量的值;- 如果函数的形参是指向非
const
变量的指针变量,实参只能使用指向非const
变量的指针;更多形参和实参指针变量关系看下表;
形参 | 实参 | 合法否 | 改变指针所指向的变量的值 |
---|---|---|---|
指向非const 变量的指针 |
非const 变量的地址 |
合法 | 可以 |
指向非const 变量的指针 |
const 变量的地址 |
不合法 | |
指向const 变量的指针 |
const 变量的地址 |
合法 | 不可以 |
指向const 变量的指针 |
非const 变量的地址 |
合法 | 不可以 |
②指向常对象的指针
- 和指向常变量的指针类似,只能用指向常对象的指针变量指向它,不能用指向非
const
对象的指针指向它; - 如果定义了一个指向常对象的指针变量,并指向了一个非
const
对象,则不能通过改指针变量来改变变量的值;
1 | Time t1(10, 10, 30); |
- 使用意义也是在形参上,希望调用函数的时候对象的值不被改变,就把形参指定为指向常对象的指针变量;
静态成员
①静态数据成员
- 静态数据成员在内存中只占一份空间(不是每一个对象都为它保留一份空间),静态数据成员的值对所有对象都是一样的,如果改变它的值,在其他对象中也会改变;
- 静态数据成员只能在类体外进行初始化。如
int Box::height = 10;
,只在类体中声明静态数据成员时加上static
,不必在初始化语句中加static
。 - 不能在参数初始化列表中对静态数据成员进行初始化;
②静态成员函数
静态成员函数和普通函数最大的区别就在静态成员函数没有
this
指针,决定了静态成员函数与不能访问本类中的非静态成员。所以静态成员函数的作用就是用来访问静态数据成员;但是普通成员函数(非静态)可以引用类中的静态数据成员;
友元
① 将普通函数声明为友元函数
这样这个普通函数就可以访问声明了友元函数的那个类的私有成员;
1 |
|
输出:
1 | 10:10:30 |
② 将别的类中的成员函数声明为友元函数
1 |
|
③ 友元类
- 在
A
类中声明B
类是自己的友元类,这样B
类就可以访问A
的所有私有成员了。 - 但是要注意友元的关系是单向的而不是双向的。且友元的关系不能传递;
C++重载运算符
重载基本运算符
案例一: 重载+
号来计算复数
1 |
|
输出:
1 | (4,6i) |
案例二: 重载双目运算符
- 注意这个要返回的是引用,因为运算符支持连续的相加操作;
- 在成员函数中返回
this
指针指向的内容,在友元函数中可以返回第一个参数的引用(不是局部变量的,所以可以返回);
1 |
|
案例三: 重载单目运算符
1 |
|
输出:
1 | (2,3i) |
重载=
号操作符
- 这个重点是在当类中有指针的时候,就要注意堆中分配空间的问题,如果不是在初始化的时候使用的
=
操作符,就是代表的赋值,其中的指针不能使用浅拷贝; - 需要我们重写
=
操作符,实现深拷贝。(就是不能让两个对象同时指向堆中的同一块内存,因为释放内存的时候不能释放两次);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
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
class Student{
public:
Student() {
this->id = 0;
this->name = NULL;
}
Student(int id, const char *name){
this->id = id;
int len = strlen(name);
this->name = new char[len+1];
strcpy(this->name, name);
}
Student(const Student& thr){ // 拷贝构造
this->id = thr.id;
//深拷贝
int len = strlen(thr.name);
this->name = new char[len+1];
strcpy(this->name, thr.name);
}
//重载=号,防止 s2 = s1的时候内部的name直接指向堆中的同一个内容,析构时发生错误
Student& operator= (const Student& thr){
//1. 防止自身赋值
if(this == &thr) //&是取地址
return *this;
//2. 将自身的空间回收
if(this->name != NULL){
delete[] this->name;
this->name = NULL;
this->id = 0;
}
//3. 执行深拷贝
this->id = thr.id;
int len = strlen(thr.name);
this->name = new char[len + 1];
strcpy(this->name, thr.name);
//4. 返回本身的对象
return *this;
}
~Student(){
if(this->name != NULL){
delete[] this->name;
this->name = NULL;
this->id = 0;
}
}
void printS(){
printf("%s\n", name);
}
private:
int id;
char *name;
};
int main(int argc,const char **argv)
{
Student s1(1, "zhangsan");
Student s2(s1); //拷贝构造
Student s3 = s1; //这个调用的还是拷贝构造函数,不是=操作符,初始化的都是拷贝构造函数
s1.printS();
s2.printS();
s3.printS();
//但是这样就是调用的等号操作符
Student s4;
s4 = s1; //不是初始化的时候
s4.printS();
return 0;
}
重载流插入运算符和流提取运算符
- 重载流插入运算符的基本格式:
istream& operator>> (istream&, 自定义类&);
,也就是返回值和第一个参数必须是istream&
类型; - 重载流提取运算符的基本格式:
ostream& operator<< (istream&, 自定义类&);
,也就是返回值和第一个参数必须是ostream&
类型; - 注意: 重载这两个运算符的时候,只能将他们作为友元函数,不能作为成员函数。避免修改
c++
的标准库。
案例:
1 |
|
综合案例-矩阵加法以及输入输出
1 |
|
运行结果:
函数模板、类模板
- 函数模板基本语法:
template <typename T>
; - 类模板基本语法:
template <class T>
,注意函数声明在外面的时候也要在函数的上面写上这个语句; - 函数模板实际上是建立一个通用函数, 其函数类型和形参类型不具体制定, 用 一个虚拟的类型来代表。 这 个通用函数就成为函数模板;
- 注意: ①函数模板不允许自动类型转化;②普通函数能够自动进行类型转化;
- <函数模板和普通函数在一起调用规则:① 函数模板可以想普通函数那样可以被重载;②
C++
编 译器优先考虑普通函数; - 类模板中的
static
关 键字: ①从 类模板实例化的每一个模板类有自己的类模板数据成员, 该 模板的所有对象共享一 个statci
数 据成员②每 个模板类有自己类模板的static
数据成员的副本;
测试程序:
1 |
|
输出:
1 | a = 20,b = 10 |
案例-简单实现的Vector类模板中的push_back
1 |
|
输出:
1 | 10 |
文件操作
输入输出流
C++
输入输出包含以下三个方面的内容:
- 对 系统指定的标准设备的输入和输出。 即从键盘输入数据,输出到显示器屏幕。这种输入输出称为标准的输入输出,简称标准 I /O。
- 以外存磁盘文件为对象进行输入和输出, 即从磁盘文件输入数据, 数据输出到磁盘文件。 以外存文件为对象的输入输出称为文件的输入输出,简称文件 I /O。
- 对 内存中指定的空间进行输入和输出。通常指定一个字符数组作为存储空间( 实际上可以利用该空间存储任何信息) 。 这种输入和输出称为字符串输入输出,简称串 I /O。
缓冲区的概念:
- 要注意;
- 读和写是站在应用程序的角度来看的;
标准输入流对象 cin
, 重点函数:
1 | cin.get() // 一次只能读取一个字符 |
测试cin.get()
、cin.getline()
的简单使用:
1 |
|
测试cin.ignore()
使用:
1 |
|
运行效果:
测试cin.peek()
函数的使用:
1 |
|
运行结果:
测试cin.putback()
函数的使用:
1 |
|
效果和测试cin.peek()
函数的一样。
标准输出流对象 cout
,重点函数:
1 | cout.flush() // 刷新缓冲区 |
cout
比较简单不做案例演示。
文件读写
输入输出是以系统指定的标准设备(输入设备为键盘,输出设备为显示器)为对象的。 在实际应用中,常以磁盘文件作为对象。即从磁盘文件读取数据,将数据输出到磁盘文件。 和 文件有关系的输入输出类主要在 fstream.h
这 个头文件中被定义,在这个头文件中主要被定义了三个类, 由这三个类控制对文件的各种输入输出操作 , 他们分别是 ifstream
、 ofstream
。
由于文件设备并不像显示器屏幕与键盘那样是标准默认设备,所以它在 fstream.h
头 文件中
是没有像 cout
那 样预先定义的全局对象,所以我们必须自己定义一个该类的对象。 ifstream
类 ,它是从 istream
类 派生的,用来支持从磁盘文件的输入。ofstream
类 ,它是从ostream
类 派生的,用来支持向磁盘文件的输出。fstream
类 ,它是从iostream
类 派生的,用来支持对磁盘文件的输入输出。
所谓打开( open
)文 件是一种形象的说法,如同打开房门就可以进入房间活动一样。 打
开 文件是指在文件读写之前做必要的准备工作,包括:
- 为文件流对象和指定的磁盘文件建立关联,以便使文件流 向指定的磁盘文件。
- 指定文件的工作方式,如,该文件是作为输入文件还是输出文件,是
ASCII
文件还是二进制文件等。
以上工作可以通过两种不同的方法实现。
1 |
|
演示结果:
测试按照二进制方式写入和读取:
1 |
|