说明:此版只是C++基础学习的笔记,涵盖内容有限,知识点细节上也有所欠缺,并未深入探讨各类机制的实现原理。所以在学习到相关知识点时建议参考其他博主的专门介绍的文章来深入学习,本篇笔记所写内容若有疑问和出错的地方,期待与您的相互交流,感谢您的支持!
一、C++介绍
C++ 是一种静态类型的、编译式的、通用的、大小写敏感的、不规则的编程语言,支持过程化编程、面向对象编程和泛型编程。
1983年在贝尔实验室诞生,在发展阶段被称为“new C”,之后被称为“C with Class”,后来取名C++,++即C语言中的自增操作符,所以说C++是C的增强和完善版本。
1.C和C++的关系:
C++是对C的升级和完善。
2.学习C++
学习 C++,关键是要理解概念,而不应过于深究语言的技术细节。
学习程序设计语言的目的是为了成为一个更好的程序员,也就是说,是为了能更有效率地设计和实现新系统,以及维护旧系统。
C++ 通常用于编写设备驱动程序和其他要求实时性的直接操作硬件的软件。
3.C的缺陷
1.不严谨
2.名字冲突 作用域
3.面向过程
4.无法解决大规模问题
4.C++新增特性
-
更为严格的类型检查
-
新增变量引用
-
支持面向对象
类和对象、 继承、 多态、 虚函数及RTTI( 运行时类型识别)
-
新增泛型编程
支持模板( template) , 标准模板库(STL) -
支持异常处理
-
支持函数及运算符重载
-
支持名字空间
用于管理函数名、 变量名及类
5.面向对象编程OOP
OOP是程序设计工程化的一种方法, 软件架构的一种思想。以事务为中心。 一切事物皆对象, 通过面向对象的方式, 将现实世界的事物抽象成对象。
OOP基本原则是程序是由单个能够起到子程序作用的单元或对象组合而成, 以达到软件工程的三个主要目标: 重用性、 灵活性和扩展性。
-
重用性:代码被重复使用, 以减少代码量, 就是重用性的重要指标。
-
灵活性:软件系统由很多可以复用的构件随意重构, 以达到实现不同的功能, 非常灵活。
-
扩展性:软件系统可以容易地新增需求, 基本构件也可以轻松的扩展功能
面向过程设计:
程序 = 数据结构 + 算法
面向对象设计:
对象 = 数据结构 + 算法
程序 = ( 对象 + 对象 + ...) + 对象间通讯机制
6.编译C++程序
C和C++的编译步骤一样
(1)预处理
(2)编译
(3)汇编
(4)链接
7.内联函数
C++提供一种提高效率的方法, 即在编译时将所调用函数的代码直接嵌入到主调函数中。 这种嵌入到主调函数中的函数称为内置函数(inline function), 又称内嵌函数或内联函数。
语法:inline 存储类型 数据类型 函数名(参数列表);
例子:
#include <iostream>
using namespace std;
inline void fun(void)
{
cout << "call fun" << endl;
}
int main()
{
int n = 0;
while(n < 5)
{
fun();
n++;
}
//相当于上面的循环语句里fun();被替换成cout << "call fun" << endl;
return 0;
}
注意:内联函数的限制
使用内联函数可以节省运行时间,但却增加了目标程序的长度。是以空间换时间的一种方法。
因此一般只将规模很小(一般为5个语句以下)而使用频繁的函数声明为内置函数。
由于多个源文件是分开编译的,要内联就应该把内联函数声明定义在头文件中。
内置函数中不能包括复杂的控制语句,如循环语句和switch语句。
8.函数重载
在大规模代码中,函数重名难以避免,为此提出了函数重载。
在同一个作用域下,函数名相同,参数列表不同(个数不同,类型不同,个数和类型都不同),返回值可相同可不相同。
- 传递不同的参数,执行不同的函数
C++代码在编译时会根据参数列表对函数进行重命名,例如void add(int a, int b)会被重命名为_add_int_int,void add(float x, float y)会被重命名为_add_float_float。当发生函数调用时,编译器会根据传入的实参去逐个匹配,以选择对应的函数,如果匹配失败,编译器就会报错,称为重载决议。
#include <iostream>
using namespace std;
int add(int x )
{
return x+x;
}
int add(int x, int y)
{
return x+y;
}
float add(float a, float b)
{
return a+b;
}
double add(double a, double b)
{
return a+b;
}
char add(char a, char b)
{
return a+b;
}
double add(double a, double b, double c)
{
return a+b+c;
}
int main()
{
//传递不同的参数,执行不同的函数
cout << add(10) << endl;
cout << add(5, 6) << endl;
cout << add(1.1,1.2,1.3) << endl;
cout << add('1', '1') << endl;
cout << add(1.2, 2.4) << endl;
cout << add(1.2f, 2.4f) << endl;
float a = 1.2, b = 2.4;
cout << add(a, b) << endl;
return 0;
}
//小数默认double类型
要想将其视为float类型,可在小数后面加f
或者定义一个float变量来保存小数的值,然后传变量
9.默认参数
对于定义的函数,可以对其设置默认的参数,当调用函数时,传入的参数的个数少于函数参数的个数的时候,就会对其其他的变量调用默认值进行赋值
- 如果给某一参数设置了默认值,那么在参数表中其后所有的参数都必须也设置默认值。
//由于参数的传递顺序是从右至左入栈,所以有默认值的参数必须在放在形参列表的最右边!所以在给参数列表设置默认参数时时,最好从右往左设置。
- 如果进行了函数声明,在声明中设置默认值即可
#include <iostream>
using namespace std;
int add(int x=100);
int add(int x, int y,int z=20);
int main()
{
cout << add() << endl;
cout << add(20) << endl;
cout << add(20,10) << endl;
return 0;
}
int add(int x)
{
return x+x;
}
int add(int x, int y,int z)
{
return x+y+z;
}
二、内存模型和名字空间
1.作用域
描述了一个名字在文件( 编译单元) 的多大范围内可见。
(1)局部域
局部域是指在函数定义或者函数块中的程序文本部分。
(2)名字空间域
即除去函数声明、定义以及函数块内的部分。
- 最外层名字空间域——全局域,对象函数类型及模板可在全局域定义。
- 每个用户声明的名字空间域都是一个不同的域
(3)类域
每个类定义都引入一个独立的类域
2.链接性
**外部链接:**名称可以在文件间共享。
内部链接
:名称仅能在一个文件中的函数共享。
默认情况下,函数的链接性为外部的。要引用函数,可加extern限定符。
加static限定符的函数,链接性为内部。如果和外部函数重名,则静态函数替换之。
3.名字空间
C++中的一个机制,为解决多个模块间名字冲突的问题。
namespace 名字空间名
{
定义成员
}
在使用时候:
名字空间名::成员
//::即作用域限定符,为运算符中等级最高的,
使用using声明可只写一次限定修饰名。using声明以关键字using开头,后面是被限定修饰的名字空间成员名;
using hsj::pi;//hsj名字空间里的成员pi就可以直接使用
使用using指示符可以一次性地使用名字空间中所有成员都可以直接使用,比using声明方便
using namespace 名字空间名;
例:using namespace std;//就可以直接使用标准c++库中的所有成员
三、输入输出
C和C++本身都没有为输入输出提供专门的语句结果。输入输出由I/O库定义。
C++用流(stream)方式实现输入输出。定义流对象时,系统会开辟一段缓冲区用于暂存输入输出流数据,直到缓冲区满或遇到\n、endl、ends、flush才会将缓冲区所有数据一起输出并清空缓冲区。
输出:
cout 对象
输入:
cin 对象
cout输出时,系统会自动判别输出数据的类型;cin输入也会自动识别输入数据的类型。
C++的数据类型如int、char、double等和C的用法一致,不过多了一种bool数据类型,关键字类型为
bool
,定义出来的变量只有true和false两个,分别表示真和假两个值,在内存上一般只占一个字节。
注意:
string
类型的字符串并不是一种C++的标准数据类型,而是string类,一个标准类。
相比C的char *字符串更为安全和使用方便。
#include <string>//引入sting头文件,注意这和string.h是不一样的,且不是其C++新标准后的升级版
using namespace std;
string s;//声明一个字符串变量(对象),且为空字符串
//字符串操作函数
= 赋值
swap() 交换两个字符串的内容
+= 追加,在尾部添加字符
+ 串联字符串
== <=... 比较字符串
insert() 插入字符
size() 返回字符个数
length() 返回字符个数
C++提供的由C++字符串得到对应的C_string的方法是使用data()、c_str()和copy(),其中,data()以字符数组的形式返回字符串内容,但并不添加’\0’。c_str()返回一个以‘\0’结尾的字符数组,而copy()则把字符串的内容复制或写入既有的c_string或字符数组内。C++字符串并不以’\0’结尾。建议是在程序中能使用C++字符串就使用,除非万不得已不选用c_string。
四、类和对象
1.结构体
一般的, 使用面向过程的设计方法, 结构体主要是用于格式化数据, 但从面向对象的角度来看, 就是一种封装形式, 这个是面向对象实现的基础。
C++的结构体成员除了可以是变量,也可以是函数。例如
#include <iostream>
using namespace std;
struct Student
{
char name[32];
int age;
float score;
void fun()
{
cout <<"姓名:"<< name <<endl;
cout <<"年龄:"<< age <<endl;
cout <<"成绩:"<< score <<endl;
}
void fun1()
{
cout<<"上课了" <<endl;
}
};
int main()
{
//struct Student stu;
Student stu;//C++中可以不用写struct,而直接用结构体类型名来定义变量
cin >> stu.name;
cin >>stu.age;
cin >> stu.score;
stu.fun();
stu.fun1();
return 0;
}
这样设计的结构体就会很复杂,在C++中
另外,C++语法中相对C语法增加了访问权限的概念,有三种:public、 private及protected
public: 公共成员, 表示可以通过结构体变量对象直接访问到成员
private : 私有成员, 表示仅结构体成员函数可以使用的成员
protected: 保护成员, 表示被继承的派生对象可以访问使用的成员
结构体默认是public,而类默认是private
为什么不可以直接修改成员变量,而是通过成员函数去修改成员变量
因为通过成员函数去修改,就能加一些限制条件,因为成员变量在实际使用中要求是合法值,对非法值应当检测并处理,不然会影响整个程序的运行。
#include <iostream>
using namespace std;
class A
{
public:
void set_val(int n)
{
if(n<0 || n>1000)
{
val=0;
}
else
{
val=n;
}
}
void print_val(void)
{
cout << "val=" <<val <<endl;
}
private:
int val;
};
int main()
{
A a;//这种定义的对象,会存储在栈区
a.set_val(10);
a.print_val();
a.set_val(-3);
a.print_val();
//而用malloc,会放在堆区,但C++一般不用maloc,而用new,也是存储在堆区
A *p = new A;
p->set_val(10);
p->print_val();
p->set_val(-3);
p->print_val();
return 0;
}
构造、析构函数
#include <iostream>
using namespace std;
class A
{
public:
A()
{
cout << "构造函数:用来初始化,定义对象时自动调用" << endl;
cout <<"第"<< __LINE__ <<"行"<<endl;
}
A(int n)
{
val=n;
cout << "构造函数:用来初始化,定义对象时自动调用" << endl;
cout <<"第"<< __LINE__ <<"行"<<endl;
}
~A()
{
cout <<"析构函数:用来回收资源 ,对象销毁时自动调用" <<endl;
cout <<"第"<< __LINE__ <<"行"<<endl;
}
void print_val(void)
{
cout << "val=" <<val <<endl;
}
private:
int val;
};
int main()
{
A a;
a.print_val();
A a1(99);
a1.print_val();
return 0;
}
3.static关键字
和C一样的部分:
修饰局部变量,延长其变量的生命周期(从栈区移动到数据段区)
修饰全局变量,限制作用域,只能在本文件中使用
修饰函数,限制作用域,只能在本文件中使用
C++相比C新增部分:
static+成员变量
(1)static修饰的成员变量不占class的空间
类没有私有成员变量时,也会占1个字节。在有私有变量时,该字节会作为变量的存储空间的一部分,如有一个int a,则类定义的对象的大小为4字节。
(2)修饰成员变量,需要在类的外部单独定义
(3)多个对象访问到的static变量是同一个,也就是这个static变量的地址是同一个地址
(4)不使用对象也可以访问static成员变量。
故可以通过static实现对象间通信:
#include <iostream>
#include <string.h>
using namespace std;
class Demo{
public:
void set_buf(const char *p)
{
strcpy(buf,p);
}
void printf_buf()
{
cout<<"buf="<<buf<<endl;
}
private:
static char buf[64];
};
char Demo::buf[64]={0};
int main()
{
Demo obj1;
Demo obj2;
obj1.set_buf("hello world");
obj2.printf_buf();
obj2.set_buf("yes");
obj1.printf_buf();
}
通过static统计对象个数
#include <iostream>
using namespace std;
class Point{
public:
Point(int x=0,int y=0)
{
this->x=x;
this->y=y;
count++;
}
~Point()
{
count--;
}
void set_point(int a,int b)
{
x=a;
y=b;
}
void show_point()
{
cout<<"("<<x <<","<< y<<")"<<endl;
}
static int getpointnum()
{
return count;
}
private:
int x;
int y;
static int count ;
};
int Point::count=0;
int main()
{
cout<<"count="<<Point::getpointnum()<<endl;
Point p;
p.show_point();
Point p1;
p1.set_point(1,1);
p1.show_point();
Point p2;
p2.set_point(2,2);
p2.show_point();
cout<<"count="<<p2.getpointnum()<<endl;
}
static+成员函数
(1)不拥有this指针,不能访问普通成员变量,可以访问静态成员变量
(2)不用对象,也可以访问成员函数
4.const关键字
const+变量:
表示变量为只读变量,
- 不能被修改
- 定义该只读变量时必须初始化
const+指针
原则:const修饰谁,谁的内容就不可变,其他都可变
第一种:const int *p=&a;
const 和int可以互换位置,二者等价。int const *p=&a;也属第一种。
const修饰*p,(可以理解为修饰p所指的空间)则不能修改p所指空间的内容,但p可以改变自己的指向。另外这只是禁止p去修改所指空间,其他指向这片空间的且没有被const修饰的指针是可以去修改空间内容的。
这种用法常用于定义函数的形参,前面学习 printf 和 scanf,以及后面将要学习的很多函数,它们的原型中很多参数都是用 const 修饰的,这样做的好处是安全!我们通过参数传递数据时,就把数据暴露了。而大多数情况下只是想使用传过来的数据,并不想改变它的值,但往往由于编程人员个人水平的原因会不小心改变它的值。这时我们在形参中用 const 把传过来的数据定义成只读的,这样就更安全了。这也是 const 最有用之处。
第二种:int *const p=&a;
此时const修饰p,则p保存的内容(地址)是不能改变的,也就是指针p的指向不能修改。但指针所指空间的内容可以修改。
第三种:const int * const p =&a;
最右边的const修饰p,左边的const修饰*p,则不能改p的指向,也不能改p所指变量的值
const+结构体变量
struct Data{
int year;
int month;
int day;
};
int main()
{
const Data d={1970,1,1};
//d.year=2022;//error!不能通过结构体变量修改结构体成员的值
Data d2={2000,1,1};
d=d2;//error 不能给结构体变量进行赋值
}
const+成员变量
成员变量不能被修改。
注意:要给const成员变量赋初值,可以通过构造函数的初始化列表来赋初值,相对灵活。
初始化列表形式:
类名(形参列表) : 成员变量(初值), 成员变量(初值),....
{
}
例子:
#include <iostream>
using namespace std;
class A
{
public:
//构造函数初始化列表
A(int n=0,int m=1):val(n),x(m)
{
}
~A()
{
}
void set_val(int n)
{
//val=n;//error!成员变量的值不能被修改
}
void print_val()
{
cout<<"val="<<val<<endl;
}
void print_x()
{
cout<<"x="<<x<<endl;
}
private:
const int val;
int x;
};
int main()
{
A a;
a.print_val();
a.print_x();
A b(2,8);
b.print_val();
b.print_x();
return 0;
}
const+成员函数
不能修改函数体内的成员变量
注意:const加在{}前面
#include <iostream>
using namespace std;
class A
{
public:
//构造函数初始化列表
A(int n=0,int m=1):val(n),x(m)
{
}
~A()
{
}
void set_val(int n)
{
//val=n;//error!成员变量的值不能被修改
}
void print_val()
{
cout<<"val="<<val<<endl;
}
void print_x() const
{
cout<<"x="<<x<<endl;
//x=10;//error! 'A::x' in read-only object,不能修改
}
private:
int val;
int x;
};
int main()
{
A a;
a.print_val();
a.print_x();
A b(2,8);
b.print_val();
b.print_x();
return 0;
}
const+对象
const +类 +对象;必须先初始化,和const int i=10相似
- 不能通过成员函数修改
- 使用的成员函数也要是const修饰
- 当成员变量的权限为public时,可以访问,但是不能修改
4.友元
friend
指类的朋友,使用友元可以访问类的私有成员
友元函数
修饰普通函数,则将其变为友元函数。也就是将函数声明放在类里,并加上friend修饰符。
友元函数也是普通函数,但它可以访问某个类中的私有成员
#include <iostream>
using namespace std;
class A
{
public:
A(int n=1)
{
val=n;
}
~A()
{
}
void set_val(int n)
{
val=n;
}
void print_val()
{
cout<<"val="<<val<<endl;
}
friend void fun(A &obj);//友元函数,在普通函数前面加上friend关键字进行声明
private:
int val;
};
void fun(A &obj)
{
cout<<obj.val<<endl;
}
int main()
{
A a;
fun(a);
return 0;
}
注意:友元会破坏类的封装,尽量少用,Java就是没有友元的
而C++保留友元,是为
提高程序的运行效率
。类的非公有成员,在类外访问需要通过函数调用和返回来实现,若定义友元函数,则不需要,效率会提高。
//例子,求两点间距离
#include <iostream>
#include <math.h>
using namespace std;
class Point{
public:
void set_point(int a,int b)
{
x=a;
y=b;
}
void print_point()
{
cout<<"("<<x <<","<< y<<")"<<endl;
}
friend double distance(Point &p1,Point &p2);
private:
int x;
int y;
};
double distance(Point &p1,Point &p2)
{
int a=p1.x-p2.x;
int b=p1.y-p2.y;
return sqrt(a*a+b*b);
}
int main()
{
Point p1;
Point p2;
p1.set_point(1,1);
p2.set_point(2,2);
p1.print_point();
p2.print_point();
double ret=distance(p1,p2);
cout<<"distance="<<ret<<endl;
}
友元类
修饰一个类,将该类定义为友元类。
如在A类中加入 friend B类,则B类就是A类的友元类。
B类中的函数就可以访问A类中的所有成员
注意:
-
友元关系
不具有传递性
。例如,类A是类B的友元,类B是类C的友元,但并不表示类A是类C的友元。 -
友元关系
不具有交换性
。例如,类A是类B的友元,但类B不是类A的友元。
#include <iostream>
using namespace std;
class A{
public:
A(int n = 100)
{
val = n;
}
~A(){
}
//类B是类A的朋友
friend class B;
private:
int val;
};
class B{
public:
void test(A &obj)
{
cout << obj.val << endl;
}
};
int main()
{
A a;
B b;
b.test(a);
return 0;
}
补充:new delete 与malloc free的区别
1.malloc/free 是标准库函数,而new/delete是C++的运算符。
2.C++的对象在用new/delete创建时和消亡前会自动执行构造和析构函数;如果用malloc/free操作对象,编译器没有控制权限,无法自动执行构造和析构函数。
3.new相比malloc更安全,且能自动计算需要分配的空间大小。
//另外,malloc/free 与new/delete最好配对使用,不然容易出问题。
五、运算符重载
C++中重新定义这些运算符,
可以被重载的运算符:
算术运算符:+、-、
、/、%、++、–
位操作运算符:&、|、~、^(位异或)、<<(左移)、>>(右移)
逻辑运算符:!、&&、||
比较运算符:<、>、>=、<=、==、!=
赋值运算符:=、+=、-=、
=、/=、%=、&=、|=、^=、<<=、>>=
其他运算符:[]、()、->、new、delete、new[]、delete[]
不能被重载的运算符:
逗号 三目运算符 “? :” sizeof() 作用域 “::” .成员访问运算符
实现方法有两种:
方法1: 通过成员函数实现
方法2: 通过友元函数实现
注意:1.慎用友元函数 , 2.不要同时写上两种方法,编译器无法二选一
1.二元运算符重载
C和C++的一些字符串操作方法,
#include <iostream>
using namespace std;
int main()
{
#if 0
//C语言中实现字符串的操作,是通过调相应函数实现
char buf[64]="hello";
char str[64]="world";
#endif
#if 1
//C++进行字符串的操作
string str1="hello";
string str2="world";
//可以使用类中的方法
str1.append(str2);
cout<<"append"<<str1<<endl;
//也可以用重载的运算符
string str3 = str1 + str2;
cout<<"str3="<<str3<<endl;
str1=str2;
cout<<str1<<" "<<str2<<endl;
if(str1==str2)
{
cout <<"=="<<endl;
}
else
{
cout <<"!="<<endl;
}
#endif
}
例子:+运算符重载,用成员函数的方式
#include <iostream>
using namespace std;
class A{
public:
A(int n = 0)
{
val = n;
}
~A(){
}
void set_val(int m)
{
val = m;
}
void print_val()
{
cout <<"val=" << val <<endl;
}
//运算符重载 方法一 :成员函数
A operator+(A &obj) // a1.operator+(a2);
{
cout << val << " " << obj.val << endl;
#if 0
A temp(val+obj.val);
return temp;
#endif
A temp;
temp.set_val(val + obj.val);
return temp;
}
private:
int val;
};
int main()
{
A a1(6);
a1.print_val();
A a2(4);
a2.print_val();
A a3 = a1 + a2; // a1.oprator+(a2);
a3.print_val();
return 0;
}
+运算符重载,用友元函数的方式
#include <iostream>
using namespace std;
class A{
public:
A(int n = 0)
{
val = n;
}
~A(){
}
void set_val(int m)
{
val = m;
}
void print_val()
{
cout <<"val=" << val <<endl;
}
friend A operator+(A &obj1, A &obj2);
private:
int val;
};
A operator+(A &obj1, A &obj2)
{
A temp;
temp.set_val(obj1.val + obj2.val);
return temp;
}
int main()
{
A a1(6);
a1.print_val();
A a2(4);
a2.print_val();
A a3 = a1 + a2; // operator+(a1, a2);
a3.print_val();
return 0;
}
2.一元运算符重载
例: ~运算符的重载,这里用成员函数方式。友元函数方式就把成员函数放在类外并变成普通函数,然后类内声明为友元函数。
#include <iostream>
using namespace std;
class A{
public:
A(int n = 0)
{
val = n;
}
~A(){
}
void set_val(int m)
{
val = m;
}
void print_val()
{
cout <<"val=" << val <<endl;
}
A operator~()
{
A temp(~val);
return temp;
}
private:
int val;
};
int main()
{
A a(1);
a.print_val();
A a1 = ~a; //a.operator~();
a1.print_val();
return 0;
}
例子2:两个点的 == 判断, 自增运算符的重载
#include <iostream>
using namespace std;
class Point{
public:
Point(int x, int y)
{
this->x = x;
this->y = y;
}
~Point()
{
}
void print_point()
{
cout << "(" << x <<" , " <<y << ")" << endl;
}
bool operator==(Point &obj)
{
if( (this->x == obj.x) && (this->y == obj.y) )
{
return true;
}
else{
return false;
}
}
//前++
Point operator++()
{
x++;
y++;
return *this; //返回本身
}
//后++
Point operator++(int)
{
Point temp = *this; //记录自己
x++;
y++;
return temp;
}
private:
int x;
int y;
};
int main()
{
Point p1(1,2);
p1.print_point();
Point p2(1,6);
p2.print_point();
//p1 == p2; //p1.operator==(p2);
if(p1 == p2)
{
cout << "相等" <<endl;
}
else{
cout << "不相等" <<endl;
}
#if 0
Point p3 = ++p1; //p1.operator++();
p1.print_point();
p3.print_point();
#endif
Point p3 = p2++; //p2.operator++(int);
p2.print_point();
p3.print_point();
return 0;
}
3.特殊的成员函数
1.构造函数
在创建一个新的对象时,自动调用的函数,用来进行“初始化”工作:
对这个对象内部的数据成员进行初始化。
1.特点:
1)
自动调用(在创建新对象时,自动调用)
2)构造函数的函数名,和类名相同
3)构造函数没有返回类型,但可以有参数
4)可以有
多个
构造函数(即函数重载形式)
2.种类
(1)默认构造函数
即没有参数的构造函数。
注意:对没有手动定义的默认构造
1)如果数据成员使用了“类内初始值”,就使用这个值来初始化数据成员。【C++11】
2)否则,就使用默认初始化(实际上,不做任何初始化)
对手动定义的默认构造:如
#include <iostream>
using namespace std;
class A{
public:
A()
{
a=10;
}
print_a()
{
cout<<"a="<<a<<endl;
}
private:
int a=2;
};
int main()
{
A atest;
atest.print_a();
}
手动定义的默认构造就会覆盖类内初始值
(2)自定义构造…
也就是构造的重载函数
针对上面的A类如
A(float c)
{
a=c;
}
(3)拷贝构造…
在定义对象时可用同一类的另一个对象来初始化该对象的存储空间,这时所用的构造函数称为
拷贝构造函数
。
-
默认的拷贝构造也称合成的拷贝构造函数,由编译器生成,且为浅拷贝。当数据成员中没有指针时,浅拷贝是可行的;但当数据成员
中有指针时
,如果采用简单的浅拷贝,则两类中的两个指针将
指向同一个地址
,当对象快结束时,会
调用两次析构函数
,而导致指针悬挂现象,所以,此时,必须采用深拷贝。
深拷贝与浅拷贝的区别就在于深拷贝会在堆内存中另外申请空间来储存数据,从而也就解决了指针悬挂的问题。
注:默认的赋值函数也是浅拷贝
深拷贝由自己写赋值函数或拷贝构造函数,不仅可以进行数据的拷贝,也可以为成员分配内存空间,实现真正的拷贝。
例:
#include <iostream>
using namespace std;
//顺序栈 数组 top
class Stack{
public:
//创建
Stack(int n)
{
data = new int [n];
top = -1;
len = n;
}
//销毁
~Stack()
{
delete [] data;
top = -1;
len = 0;
}
//判空
bool stack_is_empty()
{
if(top == -1)
{
return true;
}
else{
return false;
}
}
//判满
bool stack_is_full()
{
if(top+1 == len)
{
return true;
}
else{
return false;
}
}
//求表长
int length()
{
return top+1;
}
//压栈
void stack_in(int n)
{
if(stack_is_full())
{
cout << "满了" << endl;
}
else{
top++;
data[top] = n;
}
}
//弹栈
int stack_out()
{
if(stack_is_empty())
{
cout << "空的" << endl;
}
else{
int m = data[top];
top--;
return m;
}
}
//清空
void clear()
{
top = -1;
}
//打印
void diaplay()
{
int i;
for(i=0; i<=top; i++)
{
cout << data[i] << " ";
}
cout << endl;
}
//深拷贝--赋值运算符重载
Stack operator=(Stack &obj)
{
cout <<"自己写赋值函数"<<endl;
int i;
for(i=0; i<=obj.top; i++)
{
data[i] = obj.data[i];
}
top = obj.top;
len = obj.len;
}
//拷贝构造
Stack(Stack &obj)
{
cout << "自己写拷贝构造函数" << endl;
int i;
for(i=0; i<=obj.top; i++)
{
data[i] = obj.data[i];
}
top = obj.top;
len = obj.len;
}
private:
int *data;
int top;
int len;
};
int main()
{
Stack s1(5);
Stack s2(5);
s1.stack_in(1);
s1.stack_in(2);
s1.stack_in(3);
s1.stack_in(4);
s1.stack_in(5);
s2.stack_in(11);
s2.stack_in(22);
s2.stack_in(33);
s2.stack_in(44);
s2.stack_in(55);
s1.diaplay();
s2.diaplay();
s1 = s2; //赋值函数
Stack s3 = s1; //拷贝构造 等价于 Stack s3(s1);
s1.diaplay();
s2.diaplay();
s3.diaplay();
return 0;
}
什么时候调用拷贝构造函数
\1. 调用函数时,实参是对象,形参不是引用类型,如果函数的形参是引用类型,就不会调用拷贝构造函数
\2. 函数的返回类型是类,而且不是引用类型
\3. 对象数组的初始化列表中,使用对象。
(4)赋值构造…
1.
对象以值传递方式从函数返回,且接受返回值的对象已经初始化过
int main() {
A c;
c = f();
printf ("global address: %x, point: %x, value: %d\n", &c, c.x, c.y);
return 0;
}
2.
对象直接赋值给另一个对象,且接受值的对象已经初始化过
int main() {
A a(1);
A c;
c = a;
printf ("global address: %x, point: %x, value: %d\n", &c, c.x, c.y);
return 0;
}
对象以值传递方式从函数返回时,若接受返回值的对象已经初始化过,
则会调用赋值构造函数,且该对象还会调用析构函数,当对象中包含指针时,会使该指针失效
,因此需要重载赋值构造函数,使用类似深拷贝或移动构造函数的方法赋值,才能避免指针失效
2.析构函数
一、定义
-
作用:对象消亡时,自动被调用,用来释放对象占用的空间
2.特点:
(1) 名字与类名相同
(2) 在前面需要加上”~”
(3) 无参数,无返回值
(4)
一个类最多只有一个
析构函数
(5) 不显示定义析构函数会
调用缺省析构函数
对不同情况下的释放时机和顺序参考https://www.cnblogs.com/puyangsky/p/5319470.html
六、模板template
模板,代码复用技术
- 模板是一种对类型进行参数化的工具;
通常有两种形式:函数模板和类模板;
(1)函数模板针对仅参数类型不同的函数;
template <class 形参名,class 形参名,......> 返回类型 函数名(参数列表)
{
函数体
}
//class关键字可以用typename代替,两者没区别
例子:
template <class T> void swap(T& a, T& b){
}
调用该函数:
swap(1,2);//表示a,b参数都是int,模板函数类型T会被调用时的类型所代替
//注意T只能为一种类型,不能swap(1,1.3),这相当于为T指定两种数据类型,会出错。
(2)类模板针对仅数据成员和成员函数类型不同的类
//类模板定义一般放在头文件
template<class T> class A{
public:
T g(T a,T b);
A();
};
//在主函数或者功能文件可对模板实例化
#include<iostream.h>
#include "TemplateDemo.h"
//类成员函数的定义
template<class T> A<T>::A(){}
template<class T> T A<T>::g(T a,T b){
return a+b;
}
void main(){
A<int> a;//类模板实例化
cout<<a.g(2,3)<<endl;
}
七、继承
1.特点和定义
-
针对类的代码复用技术
,提高开发效率,减少错误,让大规模代码开发的关注点转到软件结构上。 -
所谓“继承”,就是
在一个已经存在的类的基础上建立一个新的类
。将已存在的类称为基类(或者父类),新建立的类称为派生类(或子类)。 -
派生类继承了基类的所有数据成员和成员函数,并可以增加或调整。
一个派生类继承了所有的基类方法,但下列情况除外:
基类的构造函数、析构函数和拷贝构造函数。
基类的重载运算符。
基类的友元函数。
- 基类和派生类是相对的,一个基类可以派生出多个派生类,每一个派生类又可作为基类再派生出新的派生类。
关系:
派生类是基类的具体化,而基类则是派生类的抽象。
语法:
class 子类名 : 权限访问限定符 基类名1,权限访问限定符 基类名2,...{
//子类相比父类新增成员
};
权限访问限定符
1. public (公有继承),基类的的共有和保护成员在继承到子类里不改变属性,共有任然共有,保护任然保护。//权限不变
2. protected (保护继承),...基类的共有和保护成员都成为派生类中的保护成员//权限降级到protected
3. private (私有继承),...基类的共有和保护成员都成为派生类的私有成员//权限降级到private
不管哪种方式,基类中的 public/protected 成员可以被子类访问,在类外通过派生类对象则无法访问它们。基类的 private 成员不能直接访问,无论派生类里的成员函数还是通过类对象都无法访问基类中的私有成员。但是可以通过调用基类的公有和保护成员来访问。
//此项是可选的,如果不写,那么默认为 private
引入 protected的目的是方便子类访问该父类的的成员,将其设为 protected 成员,既能起到隐藏的目的,又避免了派生类成员函数要访问它们时只能间接访问所带来的麻烦。
2.派生类的构造及析构
1.派生类有自己的构造、析构函数,而派生类不会继承基类的构造、析构函数。
2.如果基类构造函数有参数,则可从派生类的构造函数把参数传递给基类的构造函数。
派生类对象的生命期{
基类的构造函数
派生类的构造函数
…
派生类的析构
基类的析构
}
#include <iostream>
using namespace std;
class A{
public:
A(int n = 10)
{
cout << "基类构造" << endl;
x = n;
}
~A()
{
cout << "基类析构" << endl;
}
void print_x()
{
cout << x << endl;
}
private:
int x;
};
class B:public A{
public:
//通过子类给基类传递参数
B(int n = 20):A(n)
{
cout << "子类构造" << endl;
y = n;
}
~B()
{
cout << "子类析构" << endl;
}
void print_y()
{
cout << y << endl;
}
private:
int y;
};
int main()
{
A a;
a.print_x();
B b;
b.print_x();
b.print_y();
return 0;
}
构造函数执行顺序:基类构造—子类成员对象构造–子类构造
多继承,按顺序构造(从左向右),
子类成员对象,按定义的顺序构造,(从上到下),与子类的构造函数的传参列表无关
3.多继承
C++允许多继承,但注意可能会出现菱形继承问题。当然后面的虚函数可以避免这个问题,称虚继承。
class A{
public:
void funcA();
};
class B{
private:
bool funcB() const;
};
class C: public A, public B{
...
};
//类C,同时继承了A和B
类多继承导致的类命名冲突
:
当两个或多个基类中有同名成员(例如A,B,C三个基类都有成员函数show())的时候,如果直接访问该成员(派生类D类中调用show()函数),就会产生命名冲突,编译器会进行不明确提示,因为编译器此时不知道该使用哪个基类的成员,这时需要在成员名字的前面加 类名和域解析符::,便可以 显式的指明到底使用哪个类的成员,以此消除类成员命名冲突导致的命名二义性。
例子:
#include <iostream>
using namespace std;
//基类A
class BaseA
{
public:
void show();
};
void BaseA::show()
{
cout<<"基类A的show"<<endl;
}
//基类B
class BaseB
{
public:
void show();
};
void BaseB::show()
{
cout<<"基类B的show"<<endl;
}
//派生类Derived
class Derived: public BaseA, public BaseB
{
public:
void display();
};
void Derived::display()
{
BaseA::show(); //调用BaseA类的show()函数
BaseB::show(); //调用BaseB类的show()函数
cout<<"派生类Derived的display"<<endl;
}
int main()
{
Derived obj;
obj.display();
return 0;
}
八、多态
一个接口,多种方法,应对多种类型,体现多种状态(不同的效果)。
程序在运行时才决定调用的函数,是面向对象编程领域的核心概念。
1.面向过程和面向对象编程
面向过程编程是以过程为中心,把分析解决问题的步骤流程以函数的形式一步步设计实现。
优点:
- 程序结构简单-仅由三种基本结构组成(顺序、选择、循环)
- 当我们在解决复杂问题时,通常采用的就是“分而治之”的策略,即把大问题分解为小问题,然后再各个击破这些小问题,这样整个大问题就得到了解决。所以,面向过程程序设计思想也是采用这种“分而治之”的策略,把较大的程序按照业务逻辑划分为多个子模块,然后在分工逐个完成这些子模块。最后按照业务流程把他们组织起来,最终使得整个问题得到解决。按照一定的原则,把大问题细分为小的问题然后“各个击破”,符合人们思考问题的一般规律,其设计结构更易于理解,同时这种方法也易于人们掌握,通过分解问题,降低了问题的复杂度,使得程序易于实现和维护,另外,部分分解后的小问题(子模块)可以重复使用,从而避免了重复开发,而多个子模块也可以由多人分工协作完成,提高开发效率。
- 面向过程程序设计思想倡导的方法是“自顶向下,逐步求精”,即从宏观角度考虑,按照功能或者业务逻辑划分程序的子模块,定义程序的整体结构,然后再对各个子模块逐步细化,最终分解到程序语句为止。这种方法可以使程序员全面考虑问题,使程序的逻辑关系清晰明了。它让整个开发过程从原来的考虑 “怎么做” 变成考虑 “先做什么,再做什么”,流程就更加清晰了。
缺点:
-
在面向过程程序设计时,数据和操作往往是分离的,这就导致如果数据的结构发生了变化,那么操作数据的函数不得不重新改写,这个代价是非常高的。
-
数据往往不具有封装性,很多变量都会暴露在全局,加大了被任意修改的风险。
这些缺点使它越来越难以适应大型的软件项目的开发。
面向对象编程(OPP)
面向对象编程(OOP)是以事务为中心。一切事物皆对象,通过面向对象的方式,将现实世界的事物抽象成对象。
面向对象思想认为:现实世界是由对象组成的,无论大到一个国家还是小到一个原子,都是如此。并且
对象都是由两部分组成 – 描述对象状态或属性的数据(变量)以及描述对象行为或者功能的方法(函数)
。并且与面向过程不同,
面向对象是将数据和操作数据的函数紧密结合
,共同构成对象来更加精确地描述现实世界,这是
面向过程和面向对象两者最本质的区别。
之前提到面型过程的缺点,即面向过程中数据和操作是分离的,当问题规模比较小时,需求变化不大的时候,面向过程工作都做的很好。 但是,当问题的规模越来越大、越来越复杂时,面向过程就显得力不从心了,即
修改了某个结构体,就不得不修改与之相关的所有过程函数,而一个过程函数的修改,往往又会设计到其他数据结构,在规模比较小的时候容易解决,但是当系统规模越来越大时,特别是涉及到了多人协作开发,这就非常困难,这就是著名的软件危机。
正是如此,面向对象的程序设计应运而生,它的主要特点是封装、继承和多态。封装即将数据和操作封装在一起,并避免了局部变量的暴露,而只提供接口;继承可以在原来的基础上很快的产生新的对象;多态是同一个方法调用,针对不同的对象有不同的反应,这方便了程序的设计。
如上所示,封装、继承、多态就是面向对象程序设计的三大基石。 它们是紧密相连、不可分割的。通过封装,
我们可以将现实世界中的数据和对数据进行操作的动作捆绑在一起形成类,然后再通过类定义对象,很好地实现了对现实世界事物的抽象和描述
;
通过继承,可以在旧类型的基础上快速派生得到新的类型,很好地实现了设计和代码的复用
;
同时多态机制保证了在继承的同时,还有机会对已有行为进行重新定义,满足了不断出现的新需求的需要
。
2.面向对象编程的三大基石:
1.封装
:将实现细节隐藏,使代码模块化。把成员数据和成员函数封装起来,通过公共的成员接口进行成员数据的操作。
2.继承
:扩展已存在的代码,目的是为了代码重用
3.多态
:为了接口重用。不管传递过来的是哪个类的对象,函数都能通过同一个接口调用到适应各自对象的实现方法。
3.多态形式
1.静态多态
静态多态:编译时已经决定了 (早绑定),以后都不能改变,也称编译期多态。
如:函数重载、运算符重载、模板
2.动态多态
动态多态:运行时才决定(晚绑定),也称运行期多态。
设计一个接口函数,接收不同对象,执行不同的方法。
动态多态的设计思想
:对于相关的对象类型,确定它们之间的一个共同功能集,然后在基类中,把这些共同的功能声明为多个公共的虚函数接口。各个子类重写这些虚函数,以完成具体的功能。
如:派生类、
虚函数
关键点:
1.存在继承关系
2.基类引用子类对象 或者 基类指针指向子类
3.子类方法覆盖父类方法。 virtual
3.虚函数
用virtual修饰的成员函数,就是虚函数
。
虚函数的作用就是实现多态性(Polymorphism)。
多态性是将接口与实现进行分离;用形象的语言来解释就是实现以共同的方法,但因个体差异,而采用不
同的策略。
虚函数的限制如下:
A、 非类的成员函数不能定义为虚函数
B、 类的静态成员函数不能定义为虚函数
C、 构造函数不能定义为虚函数, 但可以将析构函数定义为虚函数
D、 只需要在声明函数的类体中使用关键字“ virtual” 将函数声明为虚函数, 而定义函数时不需要使
用关键字“ virtual” 。
E、 当将基类中的某一成员函数声明为虚函数后, 派生类中的同名函数( 函数名相同、 参数列表完全一致、 返回值类型相关) 自动成为虚函数。
父类虚函数的返回值类型为基础数据类型,则子类虚函数也要严格一致
如果父类的虚函数返回某个父类(Base)的指针或引用,则子类虚函数返回值只能是Base类或其Base的派生类的引用或指针。
4.覆盖、重载、隐藏
1.覆盖也称重写,指不同区域(父类和子类)中,子类重新定义基类的虚函数。函数名相同,参数相同,返回值一样 且有 virtual关键字
2.重载,指同一区域,函数名相同,参数列表不同的成员函数。
3.隐藏也称重定义,在不同区域中,函数名相同,参数不同,无论父类中的同名函数是否含有virtual关键字,都是隐藏。
5.抽象类
一般的,使用一个类,仅仅关心public成员,故此需要隐藏类的其他的成员的方法 。
另外的,譬如设计圆和椭圆的基
类,遇到的问题是,圆关心的是半径,而椭圆则有长半径和短半径,如果简单继承,并不能解决问题。雷同的问题是:很多时候基类本身生成对象并不合理,例如,动物作为一个基类可以派生出老虎、孔雀等子类,但动物本身并不能作为任何实际明确的对象。
C++引入纯虚函数,即在虚函数后加“=0”,如 virtual void func()=0;
声明了纯虚函数的基类只是用于继承,仅作为一个接口,具体功能在其派生类中实现
声明纯虚函数是告诉编译器,“在这里声明了一个虚函数,留待派生类中定义”。在派生类中对此函数提供了定义后,它才能具备函数的功能,可以被调用。
对于虚函数,子类可以(也可以不)重新定义基类的虚函数,该行为即重写。
对于纯虚函数,子类必须提供纯虚函数的个性化实现。
含有纯虚函数的类就是抽象类。
-
抽象类没有完整的信息, 只能是派生类的基类
-
抽象类不能有实例(对象), 不能有静态成员
-
派生类应该实现抽象类的所有方法
6.虚继承
**解决菱形继承的问题。**使得在派生类中只保留一份间接基类的成员.
虚继承的目的是让某个类做出声明,承诺愿意共享它的基类。其中,这个被共享的基类就称为虚基类(Virtual Base Class),本例中的 A 就是一个虚基类。在这种机制下,不论虚基类在继承体系中出现了多少次,在派生类中都只包含一份虚基类的成员.
C++标准库中的 iostream 类
就是一个虚继承的实际应用案例。iostream 从 istream 和 ostream 直接继承而来,而 istream 和ostream 又都继承自一个共同的名为 base_ios 的类,是典型的菱形继承。此时 istream 和 ostream 必须采用虚继承,否则将导致 iostream 类中保留两份 base_ios 类的成员。
7.虚析构
在特殊情况下(多态),析构函数前加上virtual 修饰,是为了确保析构的正确执行。并通常声明为虚函数。
注意:没有虚构造的说法。
#include <iostream>
using namespace std;
class Base{
public:
Base()
{
cout <<"base 构造" <<endl;
}
//虚析构:解决多态情况下,析构函数不能正常执行的情况
virtual ~Base()
{
cout <<"base 析构" <<endl;
}
};
class Son : public Base{
public:
Son()
{
cout <<"son 构造" <<endl;
}
~Son()
{
cout <<"son 析构" <<endl;
}
};
int main()
{
//栈区
//Son s;
//堆区
//Son *p = new Son;
//delete p;
Base *p = new Son;
delete p;
return 0;
}
8.限制构造
限制用户直接定义对象,只能间接定义对象
用处:单例设计
在有限制的情况下如何间接定义对象:
#include <iostream>
using namespace std;
class A{
public:
~A()
{
}
void print_val()
{
cout<<"val="<<val<<endl;
}
friend A* get_obj();
//对2,还可以用静态成员函数
static A *Get_obj()
{
A *p=new A;
return p;
}
//1.利用protected来限制构造
#if 0
protected:
A(int n=10)
{
cout<<"限制构造"<<endl;
val=n;
}
#endif
private:
//2.利用private来限制构造
#if 1
A(int n=10)
{
cout<<"限制构造"<<endl;
val=n;
}
#endif
int val;
};
//对1,但可以用继承的方式,由子类来间接构造
class B:public A{
};
//对2.用友元函数来间接构造
A* get_obj()
{
A *p=new A;
return p;
}
int main()
{
//A a; //error!
//B b;//针对protcted
//A *p=get_obj(); //针对private的法一
A *p=A::Get_obj();//针对private的法二
return 0;
}
九、其他
1.异常处理
采用分离思想,抛出异常—捕获异常
#include <iostream>
using namespace std;
class Array{
public:
Array(int n=3)
{
if(n<0)
{
throw -1;//抛出异常,参数自己填,类似perror,可以填int型或string
}
a=new int [n];
len=n;
}
~Array()
{
delete [] a;
}
void test(int max)
{
if(max>len)
{
throw "越界";
}
}
private:
int *a;
int len;
};
int main()
{
try{//检测有无异常
//Array a(-2);
Array a;
a.test(5);
}
//捕获异常,集中处理,根据错误类型自动匹配
catch(const int i)
{
cout<<i<<endl;
}
catch(const char *p)
{
cout<<p<<endl;
}
return 0;
}
2.转换函数
-
基础数据类型转换—隐式转换 显示转换
-
转换函数:可以实现对象间的类型转换。 (本质运算符重载 eg: A –>int)
//explicit 可以避免隐式转换,可以修饰构造函数,转换函数
C++提供了标准转换函数
reinterpret_cast
const_case
dynamic_cast
char *p1;
int *p2;
//p2=(int *)p1;//C的强制转换
p2=reinterpret_cast <int *> (p1);//相比强转更安全
const char *p1;
char *p2;
p2=const_cast <char *> (p1);
class Base{
public:
};
class A :public Base{
};
dynamic_cast<int *>
3.智能指针
会自动析构
1.添加对应的头文件: #include
2.添加编译选项 -std=c++11
shared_ptr : 共享智能指针,允许多个指针指向同一对象。
unique_ptr :唯一指针,不允许多个指针指向同一对象,不允许拷贝构造 ,不允许赋值函数
weak_ptr :用于管理共享智能指针的
reset:手动回收资源
expired:检测对象是否存在
4.STL 标准模板库
标准模板库
//Standard Template Library即标准模板库
//是世界上顶级C++程序员多年的杰作,是泛型编程的一个经典范例。
https://baike.baidu.com/item/%E6%B3%9B%E5%9E%8B%E7%BC%96%E7%A8%8B/6787248?fr=aladdin
//特点:自增长
1.vector 数组
https://blog.csdn.net/wkq0825/article/details/82255984
2.list 链表
https://blog.csdn.net/yas12345678/article/details/52601578/
3.queue 队列
https://www.cnblogs.com/qingyuanjushi/p/5911090.html
//C++: 支持面向过程, 支持面向对象, 支持泛型编程(STL)