1.结构化程序设计
C语言使用结构化程序设计
程序 = 数据结构 + 算法
程序由全局变量以及众多相互调用的函数组成
算法以函数的形式实现,用于对数据结构进行操作
结构化程序设计的不足:
结构化程序设计中,函数和其操作的数据结构没有直观的联系
随着程序规模的增加,程序逐渐难以理解,很难一下子看出来:
某个数据结构到底有哪些函数对它操作?
某个函数到底是用来操作哪些数据结构?
任何两个函数之间存在怎样的调用关系?
结构化程序设计中没有“封装”和“隐藏”的概念。要访问某个数据结构中的某个变量,就可以直接访问,那么当该变量的定义有所改动的时候,就要把所有访问该变量的语句找出来修改,十分不利于程序的维护、扩充。
难以查错,某个数据结构的值不正确时,难以找到到底是哪个函数导致的。
重用:在编写某个程序时,发现其需要的某项功能,在现有的某个程序里已经有了相同或类似的实现,那么自然希望能够将那部分代码抽取出来,在新程序中使用。
在结构化程序设计中,随着程序规模的增大,由于程序大量函数、变量之间的关系错综复杂,要抽取这部分代码,会变得十分困难。
总之,结构化的程序,在规模庞大时,会变得难以理解,难以扩充,难以差错,难以重用。
如何更高效地实现函数的复用?
如何更清晰的实现变量和函数的关系?使得程序更清晰更易于修改和维护。
2.面向对象的程序设计
面向对象的程序 = 类 + 类 + 类 + ...+ 类
程序设计的过程就是设计类的过程,同时处理好类与类之间的相互关系
面向对象的程序设计方法:
将某类客观事物的共同特点(属性)归纳出来,形成一个数据结构(可用多个变量描述事物的属性);
将这类事物所能进行的行为也归纳出来,形成一个函数,这些函数可以用来操作数据结构(这一步叫“抽象”)。
然后,通过某种语法形式,将数据结构和操作该数据结构的函数“捆绑”在一起,形成一个“类”,从而使得数据结构和操作该数据结构的算法呈现出显而易见的紧密关系,这就是“封装”。
面向对象的程序设计具有“抽象”、“封装”、“继承”和多态四个基本特点
3.类和对象
从客观事物抽象出类
类里面有成员变量和成员函数
class CRectangle
{
public:
int w, h;
int Area() { return w*h; }
int Perimeter() { return 2*(w + h); }
void Init(int w_, int h_) {w = w_; h = h_;}
}; //必须有分号
int main()
{
int w,h;
CRectangle r; //r是一个对象
cin >> w >> h;
r.Init(w, h);
cout << r.Area() << endl << r.Perimeter();
return 0;
}
通过类,可以定义变量。类定义出来的变量,也称为类的实例,就是我们所说的“对象”。
C++中,类的名字就是用户自定义的类型的名字。可以像使用基本类型那样来使用它。
对象的内存分配
和结构变量一样,对象所占用的内存大小,等于所有成员变量(不包含成员函数)的大小之和。
对于上面的CRectangle类,sizeof(CRectangle) = 8
每个对象各有自己的存储空间。一个对象的成员变量被改变了,不会影响到另一个对象。
对象间的运算
和结构变量一样,对象之间可以用“=”进行赋值,但不能用“==”、“!=”、“>”、“<”、“>=”、“<=”进行比较,除非这些运算符经过了“重载”。
使用成员变量和成员函数
用法一:对象名.成员名
CRectangle r1,r2;
r1.w = 5;
r2.Init(5,4)
用法二:指针->成员名
CRectangle r1,r2;
CRectangle * p1 = & r1;
Crectangle *p2 = &r2;
p1->w = 5;
p2->Init(5,4); //Init作用在p2指向的对象上
用法三:引用名.成员名
CRectangle r1,r2;
CRectangle & rr = r2;
rr.w = 5;
rr.Init(5,4); //rr的值变了,r2的值也变
4.类的成员函数和类的定义分开写
class CRectangle
{
Public:
int w,h;
int Area(); //成员函数在此声明
int Perimeter();
void Init(int w_, int h_);
}
int CRectangle::Area()
{
return w*h;
}
int CRectangle::Perimeter()
{
return 2*(w + h);
}
void CRectangle::Init(int w_, int h_)
{
w = w_; h = h_;
}
CRectangle::说明后面的函数是CRectangle类的成员函数,而非普通函数。那么一定要通过对象或对象的指针或对象的引用才能调用。
5.类成员可以访问的范围
在类的定义中,用下列访问关键字来说明类成员可被访问的范围:
private: 私有成员,只能在成员函数内访问
public: 公有成员,可以在任何地方访问
protected: 保护成员
以上三种关键字出现的次数和先后次序都没有限制。
class className{
private:
私有属性和函数
public:
公有属性和函数
protected:
保护属性和函数
};
如果某个成员前面没有上述关键字,则缺省地被认为是私有成员
class Man{
int nAge; //私有成员
char szName[20]; //私有成员
public:
void SetName(char *szName) {strcpy(Man::szName,szName);}
};
在类的成员函数内部,能够访问
当前对象的全部属性、函数
同类的其它对象的全部属性、函数
在类的成员函数以外的地方,只能访问该类对象的公有成员
void CEmployee::setName(char * name)
{
strcpy(szname,name); //ok
}
void CEmployee::getName(char * name)
{
strcpy(name,szName); //ok
}
void CEmployee::averageSalary(CEmployee e1,CEmployee e2)
{
cout << e1.szName; //ok,访问同类其他对象私有成员
salary = (e1.salary + e2.salary) / 2;
}
int main()
{
CEmployee e;
strcpy(e.szName,”Tom123456789”); //编译错,不能访问私有成员
e.setName(“Tom”); //ok
e.salary = 5000; //ok
return 0;
}
设置私有成员的机制,叫“隐藏”
“隐藏”的目的是强制对成员变量的访问一定要通过成员函数进行,那么以后成员变量的类型等属性修改以后,只需更改成员函数即可。否则,所有直接访问成员变量的语句都需要修改。
如果将上面的程序移植到内存空间紧张的手持设备上,希望将szName[20]改为char szNAME[5],若szName不是私有,那就要找出所有类似strcpy(e.szName,”Tom123456789”);这样的语句进行修改,以防止数组越界。这样做很麻烦。
如果将szName变为私有,那么程序中就不能出现(除非在类的内部)
strcpy(e.szName,”Tom123456789”);
这样的语句,所有对szName的访问都是通过成员函数进行的,比如:
e.setName(“Tom123456789”);
那么就算szName改短了,上面的语句也不需要找出来修改,只要改setName成函数内部(加上if判断语句,如果越界则做处理,确保在里面不越界)。
6.成员函数的重载及参数缺省
成员函数也可以重载
成员函数也可以带缺省参数
#include <iostream>
using namespace std;
class Location{
private:
int x,y;
public:
void init(int x = 0, int y = 0);
void valueX(int value) {x = val;}
int valueX() {return x;}
};
int main()
{
Location A,B;
A.init(5);
A.valueX(5);
cout << A.valueX();
return 0;
}
输出5;
使用缺省参数要避免有函数重载时的二义性
#include <iostream>
using namespace std;
class Location{
private:
int x,y;
public:
void init(int x = 0, int y = 0);
void valueX(int value = 0) {x = val;}
int valueX() {return x;}
};
Location A;
A.valueX(); //错误,编译器无法判断调用哪个valueX
7.构造函数
成员函数的一种,不会再分配空间只是给对象赋初值相当于给房子装修
1)名字与类名相同,可以有参数,不能有返回值(void也不行)
2)作用是对对象进行初始化,如给成员变量赋初值
3)如果定义类时没有写构造函数,则编译器生成一个默认的无参数的构造函数
默认构造函数无参数,不做任何操作。
4)如果定义了构造函数,则编译器不生成默认的无参数的构造函数。
5)对象生成时构造函数自动被调用。对象一旦生成,就再也不能在其上执行构造函数
6)一个类可以有多个构造函数
为什么需要构造函数:
1)构造函数执行必要的初始化工作,有了构造函数,就不必专门再写初始化函数,也不用担心忘记调用初始化函数
2)有时对象没被初始化就使用,会导致程序出错。
class Complex{
private:
double real,image;
public:
void Set(double r, double i);
}; //编译器自动生成构造函数
Complex c1; //默认构造函数被调用
Complex *pc = new Complex; //默认构造函数被调用
class Complex{
private:
double real,image;
public:
void Set(double r, double i = 0);
};
Complex::Complex(double r, double i){
real = r; image = i;
}
Complex c1; //error,缺少构造函数的参数
Complex *pc = new Complex; //error,没有参数
Complex c1(2); //OK
Complex c1(2,4),c2(3.5);
Complex *pc = new Complex(3,4);
可以有多个构造函数,参数个数或类型不同
class Complex{
private:
double real, image;
public:
void Set(double r, double i);
Complex(double r, double i);
Complex(double r);
Complex(Complex c1, Complex c2);
};
Complex::Complex(double r, double i)
{
real = r, image = i;
}
Complex::Complex(double r)
{
real = r, image = 0;
}
Complex::Complex(Complex c1, Complex c2)
{
real = c1.real + c2.real;
image = c1.image + c2.image;
}
Complex c1(3),c2(1,0),c3(c1,c2);
//c1 = {3,0}, c2 = {1,0}, c3 = {4,0};
构造函数在数组中的使用
class CSample{
int x;
public:
CSample(){
cout<<”Constructor 1 Called”<<endl;
}
CSample(int n){
x = n;
cout<<”Constructor 2 Called”<<endl;
}
}
int main(){
CSample array1[2]; //输出两行Constructor 1 called
cout << “step1” <<endl; //输出step1
CSample array2[2]={4,5}; //输出两行Constructor 2 Called
cout << “step2” <<endl; //输出step2
CSample array3[2]={3}; //输出Constructor 2 Called Constructor 1 Called
cout << “step3” <<endl; //输出step3
CSample * array4=new CSample[2]; //输出两行Constructor 1 called
delete []array4;
return 0;
}
class Test{
public:
Test(int n) {} //(1)
Test(int n, int m) {} //(2)
Test() {} //(3)
};
Test array1[3] = {1,Test(1,2)};
//array1里面的三个元素分别用(1),(2),(3)初始化
Test array2[3] = {Test(2,3),Test(1,2),1};
//array1里面的三个元素分别用(2),(2),(1)初始化
Test *pArray[3] = {new Test(4), new Test(1,2)};
//只有前面的没有初始化,就不会引发对象的生成,只会有指针的生成。初始化前两个指针元素,分别各指向new出来了对象,只生成了两个对象
8.复制构造函数
只有一个参数,即对同类对象引用
形如X::X(X&)或X::X(const X &),二者选一,后者能以常量对象作为参数(适应范围广)。
如果没有定义复制构造函数,那么编译器生成默认复制构造函数。默认的复制构造函数完成复制功能。
基本概念:
class Complex{
private:
double real,imag;
};
Complex c1; //调用缺省的无参构造函数
Complex c2(c1); //调用缺省的复制构造函数,将c2初始化成c1一样
class Complex{
public:
double real,imag;
Complex() { }
Complex(const Complex & c){
real = c.real;
imag = c.imag;
cout<<”Copy Constructor called”;
}
};
Complex c1;
Complex c2(c1); //调用自己定义的复制构造函数,输出Copy Constructor called
不允许有形如X::X(X)的构造函数。
class CSample{
CSample(CSample c) {}; //不允许这样的构造函数
};
复制构造函数起作用的三种情况
1)当用一个对象去初始化同类的另一个对象时。
Complex c2(c1);
Complex c2 = c1; //初始化语句,非赋值语句
2)如果某函数有一个参数是类A的对象,那么该函数被调用时,类A的复制构造函数将被调用。
class A{
public:
A() { }
A(A & a1){
cout<<”Copy Constructor called”<<endl;
}
};
void Func(A a1) {}
int main(){
A a2;
Func(a2);
return 0;
}
//程序输出Copy Constructor called
3)如果函数的返回值是类A的对象时,则函数返回时,A的复制构造函数被调用:
class A{
public:
int v;
A(int n) { v = n; };
A(const A & a){
v = a.v;
cout<<”Copy Constructor called”<<endl;
}
};
A Func(){
A b(4);
return b;
}
int main(){
cout<<Func().v<<endl;
return 0;
}//返回值对象初始化时调用了复制构造函数,参数为返回值b,使返回值对象与b一样(复制品)。
//输出结果:
//Copy Constructor called
//4
注意:对象间的赋值并不导致复制构造函数被调用
复制构造函数并不一定是复制的工作,要看复制构造函数内部的语句
常量引用参数的使用
void fun(CMclass obj_)
{
cout<<”fun”<<endl;
}
这样的函数,调用时生成形参初始化会引发复制构造函数的调用,开销比较大。
所以可以考虑使用CMyclass & 类型作为参数。(引用和实参一样,不会引发复制函数的调用;但是如果改变了形参实参也会跟着改)
如果希望确保实参的值在函数中不应被改变,那么加上const关键字。(如果函数里面出现了改变形参的语句,编译器都会报错)
void fun(const CMyclass & obj){
//函数中任何试图改变obj值的语句都将是变成非法
}
为什么要自己写复制构造函数
9.类型转换构造函数
定义转换构造函数的目的是实现类型的自动转换。
只有一个参数,而且不是复制构造函数的构造函数,一般就可以看作是转换构造函数。
当需要时,编译系统会自动调用转换构造函数,建立一个无名的临时对象。
class Complex{
public:
double real,imag;
Complex(int i) { //类型构造函数
cout<<”IntConstructor called”<<endl;
real = i; imag = 0;
}
Complex(double r, double i){
real = r;
imag = 0;
}
};
int main()
{
Complex c1(7,8);
Complex c2 = 12;
c1 = 9; //9被自动转换成一个临时Complex对象,用类型构造函数初始化临时对象
cout<<c1.real<<”,”<<c1.imag<<endl; //输出9,0
return 0;
}
10.析构函数
1)名字与类名相同,在前面加‘~’,没有参数和返回值,一个类最多只能有一个析构函数。
2)析构函数对象消亡时自动被调用。可以定义析构函数在对象消亡前做善后工作,比如释放分配空间等。
3)如果定义类没有写析构函数,则编译器生成缺省析构函数。缺省析构函数什么也不做。
4)如果定义了析构函数,编译器就不生成析构函数。
class String{
private:
char *p;
public:
String(){
p = new char[10]; //动态分配的空间要记得析构
}
~ String();
};
String::~String()
{
delete [] p;
}
析构函数与数组
对象数组生命期结束时,对象数组的每个元素的析构函数都会被调用。
class Ctest{
public:
~Ctest() {cout << “destructor called” << endl;}
};
int main()
{
Ctest array[2]; //缺省构造函数初始化
cout << “End Main” <<endl;
return 0;
}
//main函数结束对象数组生命期结束,调用析构函数
//End Main
//destructor called
//destructor called
析构函数和运算符delete
delete运算符导致析构函数被调用
Ctest * pTest;
pTest = new Ctest; //构造函数调用
delete pTest; //析构函数调用
pTest = new Ctest[3]; //构造函数调用3次
delete [] pTest; //析构函数调用三次
若new一个对象数组,那么用delete释放时应该写[].否则只delete一个对象(调用一次析构函数)’
析构函数在对象作为函数返回值返回后被调用
class CMyclass{
public:
~CMyclass() {cout << “destructor” <<endl;}
};
CMyclass obj;
CMyclass fun(CMyclass sobj){//参数对象消亡也会导致析构函数被调用
return sobj; //函数调用返回时生成临时对象返回
}
int main()
{
obj = fun(obj);//函数对象返回值(临时对象)被用过后,该临时对象析构函数被调用
return 0;
}
//输出三个destructor,第一个是形参消亡,第二个是临时对象消亡,第三个是main函 数结束全局对象obj消亡
11.构造函数和析构函数什么时候被调用
class Demo{
int id;
public:
Demo(int i){
id = i;
cout << “id = ” << id << ”constructed” <<endl;
}
~Demo(){
cout << “id = ”<< id << ”destructed” <<endl;
}
};
Demo d1(1);
void Func()
{
static Demo d2(2); //静态局部对象,局部结束不会消亡,整个程序结束消亡
Demo d3(3); //局部对象
cout << “func” << endl;
}
int main(){
Demo d4(4);
d4 = 6; //类型转换构造函数,6自动转换成临时对象,把临时对象赋值给d4,
//临时对象使用完后就会消亡,会引发析构函数
cout << “main” << endl;
{ Demo d5(5); } //花括号里面局部变量,生存期为包含它的最内花括号
Func();
cout << “main ends” << endl;
return 0;
}
//全局变量在main直线前被初始化
//输出结果
//id = 1 constructed
//id = 4 constructed
//id = 6 constructed
//id = 6 destructed
//main
//id = 5 constructed
//id = 5 destructed
//id = 2 constructed
//id = 3 constructed
//func
//id = 3destructed
//main ends main结束引发里面的局部变量d4消亡
//id = 6 destructed 一般先构造的后析构,先析构d2,在d1
//id =2 destructed
//id = 1 destructed
对象存储空间已经有了,构造函数进行初始化,不盖房子而装修房子
析构函数不负责回收对象所占据存储空间,在对象被操作系统回收之前做一些善后的工作
new出来的东西必须delete它才消亡,不delete它不会自动消亡
12.复制构造函数在不同编译器的表现
class A{
public:
int x;
A(int x_):x(x_)
{cout << x << “constructor called”<<endl;}
A(const A & a){ //本例中dev需要const其他编译器不要
x = 2 + a.x;
cout << “copy called” << endl;
}
~A(){cout << x << “destructed called” << endl;}
};
A f(){
A b(10);
return b;
}
int main(){
A a(1);
a = f(); //返回值临时对象用复制构造函数初始化
return 0;
}
//visual studio输出结果
//1 constructor called
//10 constructor called
//10 destructor called
//copy called
//12 destructor called 临时对象消亡
//12 destructor called
//dev C++输出结果
//1 constructor called
//10 constructor called
//10 destructor called
//10 destructor called 没有调用复制构造函数,少生成一个对象节约时间优化
dev出于优化目的并未生成返回值临时对象。VS无此问题