STL容器
STL中的容器分为序列容器和关联容器两种类型。序列容器包括vector、deque和list,它们的主要区别在于它们的存储方式和访问元素的效率。关联容器包括set、map和multiset/multimap,它们使用的是二叉树结构来存储元素,因此能够快速地查找和插入元素。
STL算法
STL中的算法包括排序、查找、替换、合并、拷贝等,这些算法都是以泛型的方式实现的,即它们可以用于任何类型的数据,而不需要重复地编写代码。使用STL算法可以大大提高代码的可读性和可维护性。
STL迭代器
迭代器是STL容器和算法之间的桥梁,它们提供了一种统一的方式来访问容器中的元素。STL迭代器分为输入迭代器、输出迭代器、前向迭代器、双向迭代器和随机访问迭代器五种类型,每种类型的迭代器都有其特定的功能和限制。
STL的使用方法
使用STL非常简单,只需要包含相应的头文件即可。例如,要使用vector容器,只需要包含头文件,就可以创建一个vector对象并进行各种操作。使用STL算法也非常简单,只需要调用相应的算法函数,并传递容器或迭代器作为参数即可。
常用的STL操作
下面列出了一些常用的STL操作:
1.创建容器对象: vector<int> v; set<string> s; map<int, string> m; 2.往容器中添加元素: v.push_back(10); s.insert("hello"); m[1] = "world"; 3.遍历容器中的元素: for(auto it = v.begin(); it != v.end(); it++) { cout << *it << endl; } 4.使用STL算法: sort(v.begin(), v.end()); auto it = find(s.begin(), s.end(), "hello"); copy(v.begin(), v.end(), ostream_iterator<int>(cout, " "));
multimap
multimap的用法
multimap容器里的元素,都是pair形式的 multimap<T1,T2> mp; 则mp里的元素都是如下类型: struct { T1 first; //关键字 T2 second; //值 }; multimap中的元素按照first排序,并可以按first进行查找 缺省的排序规则是 "a.first < b.first" 为true,则a排在b前面 5 multimap的应用 一个学生成绩录入和查询系统,接受以下两种输入: Add name id score Query score name是个不超过16字符的字符串,中间没有空格,代表学生姓名。id 是个整数,代表学号。score是个整数,表示分数。学号不会重复,分数 和姓名都可能重复。 两种输入交替出现。第一种输入表示要添加一个学生的信息,碰到这 种输入,就记下学生的姓名、id和分数。第二种输入表示要查询,碰到这 种输入,就输出已有记录中分数比score低的最高分获得者的姓名、学号 和分数。如果有多个学生都满足条件,就输出学号最大的那个学生的信 息。如果找不到满足条件的学生,则输出“Nobody” 输入样例: Add Jack 12 78 Query 78 Query 81 Add Percy 9 81 Add Marry 8 81 Query 82 Add Tom 11 79 Query 80 Query 81 输出样例: Nobody Jack 12 78 Percy 9 81 Tom 11 79 Tom 11 79 #include <iostream> #include <map> //使用multimap和map需要包含此头文件 #include <cstring> using namespace std; struct StudentInfo { int id; char name[20]; }; struct Student { int score; StudentInfo info; }; typedef multimap<int,StudentInfo> MAP_STD; // 此后 MAP_STD 等价于 multimap<int,StudentInfo> // typedef int * PINT; // 则此后 PINT 等价于 int *。 即 PINT p; 等价于 int * p; int main() { MAP_STD mp; Student st; char cmd[20]; while( cin >> cmd ) { if( cmd[0] == 'A') { cin >> st.info.name >> st.info.id >> st.score ; mp.insert(make_pair(st.score,st.info )); } //make_pair生成一个 pair<int,StudentInfo>变量 //其first 等于 st.score, second 等于 st.info else if( cmd[0] == 'Q' ){ int score; cin >> score; MAP_STD::iterator p = mp.lower_bound (score); if( p!= mp.begin()) { --p; score = p->first; //比要查询分数低的最高分 MAP_STD::iterator maxp = p; int maxId = p->second.id; for(; p != mp.begin() && p->first == score; --p) { //遍历所有成绩和score相等的学生 if( p->second.id > maxId ) { maxp = p; maxId = p->second.id ; } } if( p->first == score) { //如果上面循环是因为 p == mp.begin() 而终止,则p指向的元素还要处理 if( p->second.id > maxId ) { maxp = p; maxId = p->second.id ; } } cout << maxp->second.name << " " << maxp->second.id << " " << maxp->first << endl; } //lower_bound的结果就是 begin,说明没人分数比查询分数低 else cout << "Nobody" << endl; } } return 0; }
map
map的用法
和multimap区别在于:
不能有关键字重复的元素
可以使用 [] ,下标为关键字,返回值为first和关键字相同的元
素的second
插入元素可能失败
#include <iostream> #include <map> #include <string> using namespace std; struct Student { string name; int score; }; Student students[5] = { {"Jack",89},{"Tom",74},{"Cindy",87},{"Alysa",87},{"Micheal",98}}; typedef map<string,int> MP; int main() { MP mp; for(int i = 0;i < 5; ++i) mp.insert(make_pair(students[i].name,students[i].score)); cout << mp["Jack"] << endl; // 输出 89 mp["Jack"] = 60; //修改名为"Jack"的元素的second for(MP::iterator i = mp.begin(); i != mp.end(); ++i) cout << "(" << i->first << "," << i->second << ") "; //输出:(Alysa,87) (Cindy,87) (Jack,60) (Micheal,98) (Tom,74) cout << endl; Student st; st.name = "Jack"; st.score = 99; pair<MP::iterator, bool> p = mp.insert(make_pair(st.name,st.score)); if( p.second ) cout << "(" << p.first->first << "," << p.first->second << ") inserted" <<endl; else cout << "insertion failed" << endl; //输出此信息 mp["Harry"] = 78; //插入一元素,其first为"Harry",然后将其second改为78 MP::iterator q = mp.find("Harry"); cout << "(" << q->first << "," << q->second <<")" <<endl; //输出 (Harry,78) return 0; } map例题:单词词频统计程序 输入大量单词,每个单词,一行,不超过20字符,没有 空格。 按出现次数从多到少输出这些单词及其出现次数 。出现次数相同的,字典序靠前的在前面 输入样例: this is ok this plus that is plus plus 输出样例: plus 3 is 2 this 2 ok 1 that 1 #include <iostream> #include <set> #include <map> #include <string> using namespace std; struct Word { int times; string wd; }; struct Rule { bool operator () ( const Word & w1,const Word & w2) const { if( w1.times != w2.times) return w1.times > w2.times; else return w1.wd < w2.wd; } }; int main() { string s; set<Word,Rule> st; map<string,int> mp; while( cin >> s ) ++ mp[s] ; for( map<string,int>::iterator i = mp.begin(); i != mp.end(); ++i) { Word tmp; tmp.wd = i->first; tmp.times = i->second; st.insert(tmp); } for(set<Word,Rule>::iterator i = st.begin(); i != st.end(); ++i) cout << i->wd << " " << i->times << endl; }
结论
STL是C++中非常强大的一个工具,它提供了一种统一的方式来处理数据结构和算法的实现。使用STL可以大大提高代码的效率和可读性,同时也能够减少错误和bug的出现。如果你还没有使用STL,那么现在是时候学习一下了!
(三)面向对象程序设计
引用的概念
下面的写法定义了一个引用,并将其初始化为引用某个变量。 类型名 & 引用名 = 某变量名; int n = 4; int & r = n; // r引用了 n, r的类型是(教材第62页) 下面的写法定义了一个引用,并将其初始化为引用某个变量。 类型名 & 引用名 = 某变量名; int n = 4; int & r = n; // r引用了 n, r的类型是 int &(教材第62页) 下面的写法定义了一个引用,并将其初始化为引用某个变量。 类型名 & 引用名 = 某变量名; int n = 4; int & r = n; // r引用了 n, r的类型是 int & 某个变量的引用,等价于这个变量,相当于该变量的一个别名。 int n = 4; int & r = n; r = 4; cout << r; //输出 4 cout << n; n = 5; cout << r; int n = 4; int & r = n; r = 4; cout << r; //输出 4 cout << n; //输出 4 n = 5; cout << r; //输出5 定义引用时一定要将其初始化成引用某个变量。 **引用只能引用变量,不能引用常量和表达式。** double a = 4, b = 5; double & r1 = a; double & r2 = r1; // r2也引用 a r2 = 10; cout << a << endl; r1 = b; cout << a << endl; double a = 4, b = 5; double & r1 = a; double & r2 = r1; // r2也引用 a r2 = 10; cout << a << endl; // 输出 10 r1 = b; cout << a << endl; double a = 4, b = 5; double & r1 = a; double & r2 = r1; // r2也引用 a r2 = 10; cout << a << endl; // 输出 10 r1 = b; // r1并没有引用b cout << a << endl; double a = 4, b = 5; double & r1 = a; double & r2 = r1; // r2也引用 a r2 = 10; cout << a << endl; // 输出 10 r1 = b; // r1并没有引用b cout << a << endl; //输出 5
C语言中,如何编写交换两个整型变量值的函数?(与C++进行对比)
void swap( int * a, int * b) { int tmp; tmp = * a; * a = * b; * b = tmp;//利用指针进行交换 } int n1, n2; swap(& n1,& n2) ; // n1,n2的值被交换
有了C++的引用:
void swap( int & a, int & b) { int tmp; tmp = a; a = b; b = tmp; } int n1, n2; swap(n1,n2) ; // n1,n2的值被交换 int n = 4; int & SetValue() { return n; } int main() { SetValue() = 40; cout << n; return 0; } int n = 4; int & SetValue() { return n; } int main() { SetValue() = 40; cout << n; return 0; } //输出: 40
常引用
定义引用时,前面加const关键字,即为“常引用”
int n; const int & r = n; r 的类型是 const int & 不能通过常引用去修改其引用的内容: int n = 100; const int & r = n; r = 200; //编译错 n = 300; // 没问题
常引用和非常引用的转换
const T & 和T & 是不同的类型!!!
T & 类型的引用或T类型的变量可以用来初始化const T & 类型的引用。
const T 类型的常变量和const T & 类型的引用则不能用来初始化T &类型的引用,除非进行强制类型
转换。
“const”关键字的用法
1) 定义常量 const int MAX_VAL = 23; const string SCHOOL_NAME = "Peking University"; 1) 定义常量指针 int n,m; const int * p = & n; * p = 5; n = 4; p = &m; 1) 定义常量指针 不可通过常量指针修改其指向的内容 int n,m; const int * p = & n; * p = 5; //编译出错 n = 4; p = &m; int n,m; const int * p = & n; * p = 5; //编译出错 n = 4; //ok p = &m; 35 int n,m; const int * p = & n; * p = 5; //编译出错 n = 4; //ok p = &m; //ok, 常量指针的指向可以变化 不能把常量指针赋值给非常量指针,反过来可以 const int * p1; int * p2; p1 = p2; //ok p2 = p1; //error p2 = (int * ) p1; //ok,强制类型转换 函数参数为常量指针时,可避免函数内部不小心改变 参数指针所指地方的内容 void MyPrintf( const char * p ) { strcpy( p,"this"); //编译出错 printf("%s",p); //ok } 2) 定义常引用 不能通过常引用修改其引用的变量 int n; const int & r = n; r = 5; //error n = 4; //ok
动态内存分配
用new 运算符实现动态内存分配(教材P109)
第一种用法,分配一个变量: P = new T; T是任意类型名,P是类型为T * 的指针。 动态分配出一片大小为 sizeof(T)字节的内存空间,并且将该 内存空间的起始地址赋值给P。比如: int * pn; pn = new int; * pn = 5; 用new 运算符实现动态内存分配(教材P109) 第二种用法,分配一个数组: P = new T[N]; T :任意类型名 P :类型为T * 的指针 N :要分配的数组元素的个数,可以是整型表达式 动态分配出一片大小为 sizeof(T)*N字节的内存空间,并 且将该内存空间的起始地址赋值给P。 动态分配数组示例: int * pn; int i = 5; pn = new int[i * 20]; pn[0] = 20; pn[100] = 30; //编译没问题。运行时导致数组越界
用delete运算符释放动态分配的内存
用“new”动态分配的内存空间,一定要用“delete”运算符进行释放
delete 指针;//该指针必须指向new出来的空间
int * p = new int; * p = 5; delete p; delete p; //导致异常,一片空间不能被delete多次 用delete运算符释放动态分配的数组 用“delete”释放动态分配的数组,要加“[]” delete [ ] 指针;//该指针必须指向new出来的数组 int * p = new int[20]; p[0] = 1; delete [ ] p;
内联函数
函数调用是有时间开销的。如果函数本身只有几条语句,执行非常快,而且函数被反复执行很多次,相比之下调用函数所产生的这个开销就会显得比较大。
为了减少函数调用的开销,引入了内联函数机制。编译器处理对内联函数的调用语句时,是将整个函数的代码插入到调用语句处,而不会产生调用函数的语句。
1.inline int Max(int a,int b) { if( a > b) return a; return b; } 2.下面是一个简单的示例,展示如何使用内联函数来计算两个整数之和: ```c++ #include <iostream> inline int sum(int a, int b) // 定义内联函数 { return a + b; // 直接返回两数之和 } int main() { int x = 5; int y = 10; std::cout << "sum of " << x << " and " << y << " is " << sum(x, y) << '\n'; return 0; } 在编译期间,编译器通过将`sum()`函数的代码插入到主程序中,来避免函数调用时发生额外的开销。此外,由于此函数的代码块非常简短,因此提高效率的作用更加明显。
函数重载
一个或多个函数,名字相同,然而参数个数或参数类型不相同,这叫做函数的重载。
以下三个函数是重载关系: int Max(double f1,double f2) { } int Max(int n1,int n2) { } int Max(int n1,int n2,int n3) { } • 1 • 2 • 3 • 4
函数重载使得函数命名变得简单。
编译器根据调用语句的中的实参的个数和类型判断应该调用哪个函数。
下面是一个简单的示例,展示如何使用函数重载来实现不同数据类型的加法运算:
#include <iostream> // 加法运算:两个整数相加 int sum(int a, int b) { return a + b; } // 加法运算:两个浮点数相加 double sum(double a, double b) { return a + b; } int main() { // 整数相加 int x = 5; int y = 10; std::cout << "sum of " << x << " and " << y << " is " << sum(x, y) << '\n'; // 浮点数相加 double d1 = 2.5; double d2 = 3.7; std::cout << "sum of " << d1 << " and " << d2 << " is " << sum(d1, d2) << '\n'; return 0; }
在此示例中,我们定义了两个具有相同名称sum
的函数,但是输入参数的类型不同。分别为整型和浮点型,这样就可以使用函数重载来实现对不同类型数据的处理和计算。
函数缺省参数
C++中,定义函数的时候可以让最右边的连续若干个参数有缺省值,那么调用函数的时候,若相应位置不写参数,参数就是缺省值
void func( int x1, int x2 = 2, int x3 = 3) { } func(10 ) ; //等效于 func(10,2,3) func(10,8) ; //等效于 func(10,8,3) func(10, , 8) ; //不行,只能最右边的连续若干个参数缺省
函数参数可缺省的目的在于提高程序的可扩充性。
即如果某个写好的函数要添加新的参数,而原先那些调用该函数的语句,未必需要使用新增的参数,那么为了避免对原先那些函数调用语句的修改,就可以使用缺省参数。
类和对象
结构化程序设计
C语言使用结构化程序设计:
程序 = 数据结构 + 算法
程序由全局变量以及众多相互调用的函数组成。
算法以函数的形式实现,用于对数据结构进行操作。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QI5c2NTp-1688033782516)(2023-06-05-19-28-48.png)]
结构化程序设计的不足:
结构化程序设计中,函数和其所操作的数据结构,没有直观的联系。
随着程序规模的增加,程序逐渐难以理解,很难一下子看出来:
某个数据结构到底有哪些函数可以对它进行操作?
某个函数到底是用来操作哪些数据结构的?
任何两个函数之间存在怎样的调用关系?
结构化程序设计没有“封装”和“隐藏”的概念。要访问某个数据结构中的某个变量,就可以直接访问,那么当该变量的定义有改动的时候,就要把所有访问该变量的语句找出来修改,十分不利于程序的维护、扩充。
难以查错,当某个数据结构的值不正确时,难以找
出到底是那个函数导致的。
重用:在编写某个程序时,发现其需要的某项功能,在现有的某个程序里已经有了相同或类似的实现,那么自然希望能够将那部分代码抽取出来,在新程序中使用。
在结构化程序设计中,随着程序规模的增大,由于程序大量函数、变量之间的关系错综复杂,要抽取这部分代码,会变得十分困难。
总之,结构化的程序,在规模庞大时,会变得难以理解,难以扩充(增加新功能),难以查错,难以重用。
软件业的目标是更快、更正确、更经济地建立软件。
• 如何更高效地实现函数的复用?
• 如何更清晰的实现变量和函数的关系?使得程序更清晰更易于修改和维护。
面向对象程序设计和面向过程程序设计的对比
下面分别给出一个简单示例展示面向对象程序设计和面向过程程序设计之间的区别:
面向对象程序设计示例(C++)
#include <iostream> using namespace std; class Rectangle // 定义矩形类 { public: double width; // 矩形宽度 double height; // 矩形高度 // 计算矩形面积 double area() { return width * height; } // 输出矩形属性信息 void printInfo() { cout << "Width: " << width << endl; cout << "Height: " << height << endl; cout << "Area: " << area() << endl; } }; int main() { Rectangle r1; // 创建一个矩形对象 r1.width = 2.5; // 设置矩形宽度 r1.height = 3.7; // 设置矩形高度 r1.printInfo(); // 输出矩形属性信息 return 0; }
在上述代码中,我们定义了一个Rectangle
类,包括矩形的属性(宽、高)以及行为(计算面积、输出信息)。然后在主函数中,创建了一个矩形对象,并通过其成员函数实现对矩形的操作。
面向过程程序设计示例(C语言)
#include <stdio.h> // 计算矩形面积 double area(double width, double height) { return width * height; } // 输出矩形信息 void printInfo(double width, double height) { printf("Width: %.2f\n", width); printf("Height: %.2f\n", height); printf("Area: %.2f\n", area(width, height)); } int main() { double w = 2.5; // 矩形宽度 double h = 3.7; // 矩形高度 printInfo(w, h); // 输出矩形属性信息 return 0; }
在上述代码中,我们定义了area
函数和printInfo
函数来计算矩形的面积和输出矩形的属性信息。然后在主函数中,通过调用这些函数来实现对矩形的操作。
从上述两个示例可以看出,面向对象程序设计注重对象的封装、抽象和继承等特性,代码清晰、易读、易于修改,对于大型程序的开发而言具有较强的优势。面向过程程序设计则更加强调算法的设计和流程控制,并希望通过简单、清晰的代码来完成某些重复性工作。
面向对象的程序设计
面向对象的程序设计方法,能够较好解决上述问题。
面向对象的程序 = 类 + 类 + …+ 类
设计程序的过程,就是设计类的过程。
面向对象的程序设计方法:
将某类客观事物共同特点(属性)归纳出来,形成一个数据
结构(可以用多个变量描述事物的属性);将这类事物所能进行的行为也归纳出来,形成一个个函数,这些函数可以用来操作数据结构(这一步叫“抽象”)。然后,通过某种语法形式,将数据结构和操作该数据结构的函数“捆绑”在一起,形成一个“类”,从而使得数据结构和操作该数据结构的算法呈现出显而易见的紧密关系,这就是“封装”。
面向对象的程序设计具有“抽象”,“封装”“继承”“多态”四个基本特点。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZUsnCQ39-1688033782516)(2023-06-05-19-35-56.png)]
类和对象
在C++中,类(Class)是一种面向对象的概念,它描述了一个包含数据和方法(函数)的抽象实体,用来定义某个对象的属性和行为。类只是模板或蓝图,在创建对象时依据其定义,用于声明一个具有特定属性和功能的新数据类型。
而对象(Object)则是通过实例化一个类(可以理解为从类中生成一个具体的实例)而得到的一个真实存在的事物,拥有类所描述的属性和行为。通过操作对象的属性和行为,我们可以完成各种任务和操作。
例如,我们可以定义一个名为Rectangle
的类来描述矩形,如下所示:
class Rectangle { public: double width; double height; double area() { return width * height; } };
该类中包含属性width
和height
,表示矩形的长和宽,并定义了一个成员函数area()
用于计算矩形的面积。现在我们可以通过实例化这个类来创建一个真正的矩形对象,如下所示:
Rectangle r; // 创建一个矩形对象 r.width = 2.5; // 设置矩形的宽度 r.height = 3.7; // 设置矩形的高度 double a = r.area(); // 调用矩形的成员函数计算矩形的面积
这里的r
就是一个矩形对象,它包含了类Rectangle
中定义的属性和行为(成员函数),我们可以通过直接操作这些属性来进行计算,而不必关心具体的实现方式。
C++中的类和对象提供了一种抽象和封装的机制,帮助程序员更好地管理和组织代码,并以更高效、更安全的方式进行编程和设计。
对象的内存分配
和结构变量一样,对象所占用的内存空间的大小,等于所有成员变量的大小之和。
对于上面的CRectangle类,sizeof(CRectangle) = 8
每个对象各有自己的存储空间。一个对象的某个成员变量被改变了,不会影响到另一个对象。
对象间的运算
和结构变量一样,对象之间可以用 “=”进行赋值,但是不能用 “==”,“!=”,“>”,“<”“>=”“<=”进行比较,除非这些运算符经过了“重载”。
用法1:对象名.成员名
CRectangle r1,r2; r1.w = 5; r2.Init(5,4); //Init函数作用在 r2 上,即Init函数执行期间访问的 //w 和 h是属于 r2 这个对象的, 执行r2.Init 不会影响到 r1。
用法2. 指针->成员名
CRectangle r1,r2; CRectangle * p1 = & r1; CRectangle * p2 = & r2; p1->w = 5; p2->Init(5,4); //Init作用在p2指向的对象上
用法3:引用名.成员名
CRectangle r2; CRectangle & rr = r2; rr.w = 5; rr.Init(5,4); //rr的值变了,r2的值也变 void PrintRectangle(CRectangle & r) { cout << r.Area() << ","<< r.Perimeter(); } CRectangle r3; r3.Init(5,4); PrintRectangle(r3);
类成员的可访问范围
在类的定义中,用下列访问范围关键字来说明类成员可被访问的范围:
– private: 私有成员,只能在成员函数内访问 – public : 公有成员,可以在任何地方访问 – protected: 保护成员,以后再说
以上三种关键字出现的次数和先后次序都没有限制。
定义一个类 class className { private: 私有属性和函数//说明类成员的可访问范围 public: 公有属性和函数//说明类成员的可访问范围 protected: 保护属性和函数//说明类成员的可访问范围 };
如果某个成员前面没有上述关键字,则缺省地被认为
是私有成员。
class Man { int nAge; //私有成员 char szName[20]; // 私有成员 public: void SetName(char * szName){ strcpy( Man::szName,szName); } };
在类的成员函数内部,能够访问:当前对象的全部属性、函数;同类其它对象的全部属性、函数。
在类的成员函数以外的地方,只能够访问该类对象的公有成员
class CEmployee { private: char szName[30]; //名字 public : int salary; //工资 void setName(char * name); void getName(char * name); void averageSalary(CEmployee e1,CEmployee e2); }; 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,"Tom1234567889"); //编译错,不能访 问私有成员 e.setName( "Tom"); // ok e.salary = 5000; //ok return 0; } int main() { CEmployee e; strcpy(e.szName,"Tom1234567889"); //编译错,不能访 问私有成员 e.setName( "Tom"); // ok e.salary = 5000; //ok return 0;
设置私有成员的机制,叫“隐藏”
“隐藏”的目的是强制对成员变量的访问一定要通过成员函数进行,那么以后成员变量的类型等属性修改后,只需要更改成员函数即可。否则,所有直接访问成员变量的语句都需要修改。
如果将上面的程序移植到内存空间紧张的手持设备上,希望szName 改为 char szName[5],若szName不是私有,那么就要找出所有类似
strcpy(e.szName,“Tom1234567889”);
这样的语句进行修改,以防止数组越界。这样做很麻烦。
如果将szName变为私有,那么程序中就不可能出现(除非在类的
内部)strcpy(e.szName,“Tom1234567889”);这样的语句,所有对 szName的访问都是通过成员函数来进行,比如:
e.setName( “Tom12345678909887”);
那么,就算szName改短了,上面的语句也不需要找出来修改,只要改 setName成员函数,在里面确保不越界就可以了。
用struct定义类
struct CEmployee { char szName[30]; //公有!! public : int salary; //工资 void setName(char * name); void getName(char * name); void averageSalary(CEmployee e1,CEmployee e2); }; 和用"class"的唯一区别,就是未说明是公有还是私有的成员,就是公有
成员函数的重载及参数缺省
成员函数的重载(Overloading)指的是在同一个类中定义多个名称相同但参数个数或参数类型不同的成员函数,以实现类似的功能但具有不同的行为。重载可以极大提高代码的复用性和可读性,在需要使用同一函数名但行为却略有不同的情况下,使用重载能够让代码更为简洁。
重载的方式具体有两种:
- 同名不同参:函数名称相同,但是参数个数或类型不同,如下所示:
class Rectangle { public: double width; double height; double area() { return width * height; } int area(int times) { return width * height * times; } };
上述例子定义了两个area
方法,其功能都是计算矩形面积。在第一个函数中,该方法不接受任何参数,返回浮点数类型的计算结果;而在第二个函数中,该方法接受一个整型参数,并将浮点数类型的面积值乘以这个参数,最后返回整型类型的计算结果。这样在调用时,可以根据不同的需求选择不同的方法来处理数据:
Rectangle r; r.width = 2.5; r.height = 3.7; double a = r.area(); // 调用第一个area方法 int b = r.area(2); // 调用第二个area方法
- 同名同参但类型不同:函数名称和参数完全相同,但是返回值类型不同。例如,可以有一个成员函数和一个友元函数都名为
operator+()
, 其形参和行为相同,只是前者的调用方式限定在该类的对象上。
成员函数也支持参数缺省(Default Arguments)的语法,允许在定义成员函数时声明某个或某些参数的默认值,而在函数调用时如果没有传递对应的参数,则使用默认值,如下所示:
class Rectangle { public: double width; double height; double area(double rate = 1.0) { return width * height * rate; } };
上述代码中, double rate = 1.0
声明了一个默认参数,当调用该函数时如果没有指定rate值,则默认为1.0。这个特性广泛用于提高重载函数的可读性,增加使用方便性。
总之,通过使用重载和参数缺省,我们可以面向对象设计中实现更丰富、灵活和易用的编程风格来应对不同的运算需求。
使用缺省参数要注意避免有函数重载时的二义性
class Location { private : int x, y; public: void init( int x =0, int y = 0 ); void valueX( int val = 0) { x = val; } int valueX() { return x; } }; Location A; A.valueX(); //错误,编译器无法判断调用哪个valueX
构造函数(constructor)
基本概念
成员函数的一种
名字与类名相同,可以有参数,不能有返回值(void也不行)
作用是对对象进行初始化,如给成员变量赋初值
如果定义类时没写构造函数,则编译器生成一个默认的无参数的构造函数
•默认构造函数无参数,不做任何操作
如果定义了构造函数,则编译器不生成默认的无参数的构造函数对象生成时构造函数自动被调用。对象一旦生成,就再也不能在其上执行构造函数一个类可以有多个构造函数
为什么需要构造函数:
- 构造函数执行必要的初始化工作,有了构造函数,就不必专门再写初始化函数,也不用担心忘记调用初始化函数。
- 有时对象没被初始化就使用,会导致程序出错。
class Complex { private: double real, imag; public: void Set( double r, double i); }; //编译器自动生成默认构造函数 Complex c1; //默认构造函数被调用 Complex * pc = new Complex; //默认构造函数被调用 class Complex { private : double real, imag; public: Complex( double r, double i = 0); }; Complex::Complex( double r, double i) { real = r; imag = 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, imag; 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; imag = i; } Complex::Complex(double r) { real = r; imag = 0; } Complex::Complex (Complex c1, Complex c2); { real = c1.real+c2.real; imag = c1.imag+c2.imag; } Complex c1(3) , c2 (1,0), c3(c1,c2); // c1 = {3, 0}, c2 = {1, 0}, c3 = {4, 0}; 构造函数最好是public的,private构造函数 不能直接用来初始化对象 class CSample{ private: CSample() { } }; int main(){ CSample Obj; //err. 唯一构造函数是private return 0; }
在C++中,构造函数(Constructor)是一种特殊的成员函数,它用于在对象创建时初始化类的成员变量。通常,每个类都至少有一个构造函数。
构造函数的名称必须与类的名称相同,没有返回类型(void、int等),并且不能手动调用。构造函数的作用是在创建对象时为成员变量赋初值,如下所示:
class Rectangle { public: double width; double height; // 构造函数 Rectangle() { width = 0; height = 0; } };
上述代码定义了一个Rectangle
类,并在其中定义了一个构造函数,该构造函数将宽度和高度初始化为空值。
构造函数在对象被创建时自动执行,可以根据不同需求重载多个不同的构造函数。比如,在构造函数中使用参数列表为成员变量赋值:
class Rectangle { public: double width; double height; // 构造函数 #1 Rectangle(double w, double h) { width = w; height = h; } // 构造函数 #2 Rectangle() { width = 0; height = 0; } };
上述代码中定义了两个构造函数,第一个构造函数用来传入实际的宽度和高度进行初始化,而第二个构造函数不需要任何参数,则按照预设值将其初始化为0。
通过构造函数,在创建对象时就可以有效地进行成员变量的初始化,提高了代码的效率和可读性。需要注意的是,在一个类中只能有一个析构函数(Destructor),它与构造函数类似但在对象被销毁时启动执行。
另外需要注意的是,C++中的构造函数也可以通过初始化列表(Initialization List)的方式来对成员变量进行初始化,这种方式比在构造函数体中赋值效率更高,并且允许直接初始化常量和引用类型的成员变量。如下所示:
class Rectangle { public: double width; double height; // 构造函数 #1 Rectangle(double w, double h): width(w), height(h) { // 无需将参数分别复制到成员变量中 } // 构造函数 #2 Rectangle() : width(0), height(0) { // 直接将成员变量初始化为0 } };
最后还需要提出的是,如果一个类没有定义构造函数,则编译器将自动提供一个默认的构造函数,其中的成员变量都将按照其数据类型的默认规则进行初始化,如int为0、string为空等。如果需要完全掌握类的初始化过程,建议手动实现构造函数以确保代码的可靠性和安全性。
//构造函数在数组中的使用
在C++中,我们可以通过定义构造函数来自定义对象的创建和初始化过程。而当需要创建一个对象数组时,同样也可以使用构造函数来对每个对象进行初始化。
在创建对象数组时,编译器会首先调用默认的构造函数来初始化其中的所有对象。如果类定义了自己的构造函数,则编译器会使用这个构造函数来进行初始化。例如,下面的代码定义了一个Rectangle
类,并在该类中添加了一个构造函数:
class Rectangle { public: double width; double height; // 构造函数 Rectangle(double w, double h) { width = w; height = h; } };
可以按常规方式声明和实例化一个对象:
Rectangle r(3.5, 4.5);
但是如果要创建多个矩形怎么办呢?我们可以使用类似于下面示例的数组语法来进行初始化:
Rectangle rects[] = {Rectangle(1, 2), Rectangle(2, 3), Rectangle(3, 4)};
上述代码中,我们利用花括号{}列表形式来对数组元素进行初始化,其中每个元素都使用了 Rectangle
的构造函数来创建并初始化它们自己的成员变量width
和height
。
需要注意,当使用构造函数创建对象数组时,数组中的每个元素都是独立的实例,它们之间没有任何关联。因此,在不同位置修改其中任意的元素都不会影响其他元素,也就是说它们是彼此独立的。
总之,通过构造函数我们可以在创建对象时对其成员变量进行初始化,并利用数组语法来创建多个并存的对象。这种方法非常灵活,方便快捷,在实际开发中具有很高的应用价值。
除了以上提到的花括号{}列表初始化方法,还可以使用循环来初始化对象数组中的元素。例如:
Rectangle rects[3]; // 创建一个长度为3的矩形数组 for (int i = 0; i < 3; ++i) { rects[i] = Rectangle(i+1, i+2); // 使用构造函数创建并赋值给每个数组元素 }
上述代码中,我们首先创建了一个长度为3的Rectangle
类型数组,并且没有指定初始值,然后使用循环遍历数组的每个元素,使用构造函数进行初始化。需要注意的是,这种方法只有在数组已经被定义时才能使用,因为在C++中数组一旦被定义就不能改变其大小。
总之,使用构造函数初始化对象数组的方法非常灵活多样,开发者可以自由选择合适的方法来实现自己的需求。同时,也需要注意在对象数组的使用中要保证构造函数的正确性和安全性,以避免潜在的问题。
除了在对象数组中使用构造函数进行初始化外,有时候我们还需要手动调用析构函数来释放对象占用的资源。在C++中,通过使用delete操作符可以显式地调用对象的析构函数,并将其从内存中销毁。
例如,在下面这个例子中,我们定义了一个类MyClass
,并在其中添加了构造函数和析构函数:
class MyClass { public: MyClass() { /* 构造函数代码 */ } ~MyClass() { /* 析构函数代码 */ } };
此时,如果我们想要手动销毁某些对象实例,可以使用delete
操作符来调用它们的析构函数,如下所示:
MyClass* obj = new MyClass(); // 创建一个MyClass类型对象 // 在不需要obj对象时,手动释放该对象 delete obj; // 调用析构函数并将对象从内存中销毁
上述代码中,我们利用关键字new
来创建了一个对象实例,并将其保存到指向该对象的指针obj
中。然后,在不再需要该对象时,我们可以使用delete
操作符手动释放并销毁该对象,从而避免内存泄露和资源浪费问题。
需要注意的是,在调用delete操作符时,如果删除的对象是通过数组元素创建的,则必须使用delete[]
操作符来销毁该数组,如下所示:
MyClass* objs = new MyClass[10]; // 创建一个包含10个对象的数组 // 在不需要objs数组时,手动释放该数组 delete[] objs; // 调用析构函数并将数组从内存中销毁
总之,在C++中使用构造函数和析构函数,可以在对象创建和销毁时对其进行一系列操作,例如对成员变量进行初始化和资源释放等,具有极高的可定制性和实用价值。但同时也要注意对构造函数和析构函数的正确使用和合理管理,以确保代码效率和正确性。