本文共 27224 字,大约阅读时间需要 90 分钟。
#include#include using namespace std;struct inflatable{ string name; float volume; double price;};int main(){ inflatable student = { "JJP",1.1,9.9}; inflatable x=student; cout< <
初始化省略
inflatable student[100];
不同于一般结构体的是它在定义成员的时候需要指定成员所占的位数。
struct torgle_register{ unsigned int SN : 4; // 4bit for SN value unsigned int : 4; // 4bit unused bool goodIn : 1; // valid input (1 bit) bool goodTorgle : 1; // successful torgling};
union one4all{ int int_val; long long_val; double double_val;};
每次只能同时存储其中一种类型,共用体长度为其最长成员长度。例如当商品ID有的是整数,有的是字符串时,可以使用共用体来表示。其使用与结构体类似。
共用体常用于(但并非只能用于)节省内存
#includeusing namespace std;int main(void){ enum bits{ one = 1, two = 2, four = 4,eight = 8}; bits myflag; myflag = bits(four); enum spectrum{ red,orange,yellow,green,blue}; //符号常亮对应0-4 cout< <
int* ptr
:强调int是一种类型——指向int的指针 int *ptr
:强调ptr是一个int类型的值
在C++中,int *是一种复合类型,是指向int的指针。
创建指针时,计算机将分配用来存储地址的内存,但不会分配用来存储指针所指向的数据的内存。为数据提供空间是一个独立的步骤。 务必在取内容之前,把指针初始化为一个确定的、适当的地址。
指针的赋值:
int * pt;pt = (int *) 0XB80000000; //这是因为C++在类型一致方面要求更严格
C++允许将指针和整数相加,加1的结果等于原来的地址值加上指向对象占用的总字节数。两个指针相减,得到的是一个整数,即两元素之间的间隔。
typeName * pointer_name = new typeName;int * pn = new int; //此时只能通过指针来访问数据对象,这使得程序在管理内存方面有更大的控制权
int * ps = new int;//delete ps;
注意,不能释放已经释放的内存块
int * psome = new int [10];
delete [] psome;
inflatable * ps = new inflatable;(*ps).volume = 1;ps->volume = 1;delete ps;
有时也叫自由存储空间(free store)或堆(heap)。
static double fee=56.50;
自动存储和静态存储的关键在于:这些方法严格地限制了变量的寿命。变量可能存在于程序的整个生命周期(静态变量),也可能只是在特定函数被执行时存在(自动变量)
使用new创建动态数组的替代品,包含在名称空间std中
声明:vectorvt(n_elem);// 其中n_elem可以是整型常量,也可以是整型变量//using namespace std;vector vd(n);
vector类的功能比数组强大,但效率稍低
包含在名称空间std中,且需要包含头文件array
array对象的长度是固定的,使用栈(静态内存分配),而不是自由存储区,效率与数组相同。
声明:
arrayarr;
与vector不同的是,n_elem不能是变量。
#includeusing namespace std;array ai;array ad={ 1.2,2.1,3.43,4.3};
- C++常用的方式是,在for和括号之间加一个空格,而省略函数名与括号之间的空格;
- 通常,定义一个const值来表示数组中的元素个数是个好办法。在声明数组和引用数组长度时(如在for循环中),可以使用const值;
- a++意味着使用a当前值计算表达式,然后将a的值加1;而++b的意思是先将b的值加1,然后使用新的值来计算表达式;
while (guests++<5) { cout<< to close this window... */
- 注意此处表达式(guests++<10)是一个完整表达式,因为它是一个while循环的测试条件,因此该表达式的末尾是一个顺序点。所以,C++确保副作用(将guests加1)在程序进入cout之前完成。然而通过使用后缀格式可确保将guests同5进行比较后再将其值加1。
- 虽然++优先级比<要高,但是没卵用啊!
- 简记:左++是先++再比较,右++是先比较再++
- 分号标示了顺序点,在C++11中已不再使用术语“顺序点”
- 前缀格式(++a)比后缀格式(a++)效率要高
简记:有括号的先括号,抛开后缀不说,pt前面的运算符,哪个靠近pt哪个先运算
注意,逗号并不总是逗号运算符,可使用它将两个表达式合并为一个
int i,j;for(j=0,i=word.size()-1;j
逗号运算符是一个顺序点
int x; x = 20,30; cout<<
假设字符串为mate,若word是数组名,那么不能使用【word == “mate”】这种语句来判断,此时应使用C-风格字符串库中的strcmp()函数来比较。
数组名是数组的地址,用引号括起来的字符串常亮也是其地址
while(name[i]!='\0')
与while(name[i])
相同,这是因为当name[i]为空字符时,其编码将为0或falsewhile (test-expression) body//for(;test-expression;) body// 两者等效
#include#include int main(){ using namespace std; float secs = 5; clock_t delay = secs * CLOCKS_PER_SEC; //CLOCKS_PER_SEC常量等于每秒钟包含的系统时间单位数 clock_t start = clock(); while (clock() - start < delay) ; cout<<"delay end!!!"<
do bodywhile (test-expression);
#defiine BYTE char
//定义char的别名为BYTE
typedef char byte;
//定义byte为char的别名
typedef char * byte_pointer; //将byte_pointer声明为char指针#define int_ptr int *int_ptr a,b; //相当于int * a, b; 只是简单的宏替换,a是整型指针,而b之是整型变量typedef int * int_ptr;int_ptr a,b; //a, b 都为指向int的指针,typedef为int* 引入了一个新的助记符
double prices[5] = { 4.99,10.99,6.87,7.99,8.49}; for(double x: prices) //x最初表示数组prices的第一个元素 cout<<
正确示范:if (age >17 && age<35)
if (17 < age < 35)
//务必注意,这种写法编译器不会报错,上次犯过这种低级错误。这相当关于if((17<age) < 35),恒为true expression1 ? expression2 : expression3
//如果1为true,则为2;否则,为3
注意不要忘了break
switch (integer-expression){ case label1: statement(s);break; case label2: statement(s);break; ///...... default: statement(s);break;}
#include#include int main(){ using namespace std; enum { red,orange,yellow,green}; int code; cin>>code; while(code>=red && code<=green) { switch (code) { case red : cout<<"red"< >code; } return 0;}
二者都允许程序从选项中进行选择,但case的值必须为整数(包括char),且标签值必须是常量。如果选项设计到取值范围、浮点测试或两个变量的比较,则应使用if-else语句;当选项不少于3个时,应使用switch。
typeName functionName(parameterList){ //statements; return value;}
- 原型描述了函数到编译器的接口,它将函数返回值的类型(如果有的话)以及参数的类型和数量告诉编译器。
- 函数原型不要求提供变量名,有类型列表就够了。通常在原型的参数列表中可以包括变量名,也可以不包括。原型中的变量名相当于占位符,因此不必与函数定义中的变量名相同。
- 出于简化的目的,C++标准使用参数(argument)来表示实参,使用参量(parameter)来表示形参,因此参数传递参量赋值给参数。
- 函数可以有多个参数,调用函数时只需使用逗号将这些参数分开即可
值得注意的是,arr实际上并不是数组,而是一个指针!但在编写函数其余部分时可将其当做数组来使用。
#includeconst int ArSize = 8;int sum_arr(int arr[],int n);int main(){ using namespace std; int cookies[ArSize] = { 1,2,4,8}; int sum = sum_arr(cookies,ArSize); cout< <
此程序并没有将数组内容传递给函数,而是将数组的位置(地址)、包含的元素种类(类型)以及元素数目(n变量)提交给函数。有此信息后函数便可使用原来的数组。传递常规变量时,函数将使用该变量的拷贝;但传递数组时,函数将使用原来的数组。
#includeconst int ArSize = 8;int sum_arr(int *,int n);int main(){ using namespace std; int cookies[ArSize] = { 1,2,4,8}; int sum = sum_arr(cookies,ArSize); cout< <
- 将指针(包括数组名)加1,实际上是加上了一个与指针指向的类型长度(以字节为单位)相等的值。对于遍历数组而言,使用指针加法和数组下标时等效。
- 将数组类型和元素数量告诉数组处理函数时,不能使用方括号表示法来传递数组长度。而应使用如下方式:
void fillArray(int arr[] , int size);
void show_array(const double ar[], int n);
注意这不意味着原始数组必须是常量,而只是意味着不能在函数中使用ar来修改这些数据。
int cookies[20];int sum_arr(const int * begin, const int * end);int main(){ int sum = sum_arr(cookies, cookies + 20); return 0;}int sum_arr(const int * begin, const int * end){ const int * pt' int total = 0; for (pt = begin;pt!=end;pt++) total = total + *pt; return total;}
让指针指向一个常量对象,这样可以防止使用该指针来修改所指向的值
// pt 指向一个const int,即*pt的值为constint age = 40;const int * pt = &age;//只能放置修改pt指向的值,不能防止修改pt的值
将指针本身声明为常量,这样可以防止改变指针指向的位置
const float g_earth = 9.8;const float * pe = &g_earth;
注意,不允许以下方式:
int sloth = 3;const int * ps = &sloth; // 一个指向const int的指针,ps不允许修改sloth的值int * const finger = &sloth; // 一个指向int的const指针,finger只能指向sloth,但允许使用finger来修改sloth的值
- 如果数据类型本身并不是指针,则可将const数据或非const数据的地址赋给指向const的指针,但只能将非const数据的地址赋给非const指针。
- 如果由const限定的数组,那么会禁止把常量数组的地址赋给不是常量的指针,这就意味着不能将const限制的数组名作为参数传递给使用非常量形参的函数。 指向const对象的const指针:
double trouble = 2.0E30;const double * const stick = &trouble;
通常,将指针作为函数参数来传递时,可使用指向const的指针来保护数据。
void show_array(const double ar[], int n);
//函数原型int sum(int (*ar2)[4], int size); //注意,括号必不可少// int *ar2[4] 声明一个由4个指向int的指针组成的数组,而不是由一个指向由4个int组成的数组的指针//函数体int sum(int ar2[][4], int size){ int total = 0; for (int r=0;r
ar2[r][c] == *(*(ar2 + r) + c)
C-风格字符串作为参数的函数
// 以下两种声明方式完全等效,即传递的是第一个字符串的地址unsigned int c_in_str(const char * str, char ch)unsigned int c_in_str(const char str[], char ch)
注意,字符串跟字符数组的区别是一个有结束字符、一个没有。只要字符不为空值字符(\0),*str就为非零值
返回C-风格字符串的函数
函数无法返回一个字符串,但可以返回字符串的地址,这样做效率更高
#includechar * buildstr(char c,int n);int main(){ using namespace std; int times; char ch = "helloasdfasdf"; times = 2; char *ps = buildstr(ch,times); cout< < 0) pstr[n] = c; return pstr;}
使用结构编程时,可将结构作为参数传递,并在需要时将结构用作返回值使用,但按值传递结构有一个缺点。如果结构非常大,则复制结构将增加内存要求,降低系统运行速度。(最初C不允许按值传递结构)因此许多C程序员倾向于传递结构的地址,然后使用指针来访问结构的内容。C++提供了第三种选择:按引用传递。
传递和返回结构
// 结构体定义struct travel_time{ int hours; int mins;}// 结构类型函数声明travel_time sum(travel_time t1, travel_time t2);// 结构变量初始化travel_time day1 = { 5,45};// 结构类型函数定义travel_time sum(travel_time t1, travel_time t2){ travel_time total; total.mins = (t1.mins + t2.mins); total.hours = 0; return total;}
传递结构的地址
// 值传递类型void show_polar(polar dapos){ cout<< distance<
C-风格字符串和string对象的用途几乎相同,但与数组相比,string对象与结构更相似,例如可以直接将一个对象赋值给另一个对象。
const int SIZE = 5;// 函数声明void display(const string sa[], int n);// 函数调用display(list,SIZE);
#include// 函数声明void show(std::array da);void fill(std::array * pa);// array对象的定义std::array expenses;// 值传递show(expenses);// 引用传递fill(&expenses);
注意:模板array并非只能存储基本数据类型,还可存储类对象。
C++函数可以自己调用自己(main()除外),这种功能被称为递归。
包含一个递归调用的递归
void recurs(argumentlist){ statements1 if(test) recurs(arguments) statements2}
通常的方法将递归调用放在if语句中,test最终将为false,调用链将断开。
每个递归调用都创建自己的一套变量,当程序到达第5次调用时,将有5个独立的n变量,其中每个变量的值都不同。
包含多个递归调用的递归
如果递归层次较少,这将是一种精致而简单的选择。
void subdivide(char ar[],int low,int high, int level){ if(level == 0) return; int mid = (high+low)/2; ar[mid] = '|'; subdivide(ar, low, mid, level - 1); subdivide(ar, mid, high, level - 1);}
获取函数的地址
只要使用函数名(后面不跟参数)即可。若think() 是一个函数,则think 就是该函数的地址。
注意,务必区分传递的是函数的地址还是函数的返回值(带括号的是返回值)!
声明函数指针
声明指向函数的指针时,也必须指定指针指向的函数类型。这意味着声明应该指定函数的返回类型以及函数的特征标(参数列表)。// 函数原型double pam(int);// 指针类型的声明double (*pf)(int); double (*pf_test)(int) = pam; // 指向pf = pam;
- 注意,在以上程序段中,(*pf)是函数,pf是指针
- 通常要声明指向特定类型的函数的指针,可首先编写这种函数的原型,然后用(*pf)代替函数名
double *pf(int); // pf()是一个返回指针的函数double (*pf)(int); // pf是一个指向函数的指针
double pam(int);double (*pf)(int);pf = pam;double x = pam(4);double y = (*pf)(5);
深入探讨函数指针
const double * f1(const double ar[], int n);const double * f2(const double [], int);const double * f3(const double *, int);
注意:以上三种声明形式完全相同。在函数原型中,可以省略标识符
const double * (*p1)(const double *, int); // 返回值为指针的指针函数const double * (*p1)(const double *, int) = f1;//声明的同时进行初始化auto p2 = f2; //C++11可自动判断类型,auto只能用于单值初始化而不能用于初始化列表const double * (*pa[3])(const double *, int) = { f1,f2,f3}; //函数指针数组auto pb = pa; //自动推断只能用于单值初始化,而不能用于初始化列表,但声明数组pa后声明同样类型的数据就很简单了// 函数调用cout << (*p1)(av,3) << ":"<< *(*p1)(av,3) << endl; //指向的函数是返回指针类型的函数,返回的是一个指针,要得到值所以最后取了内容cout << p2(av,3) << ":" << *p2(av,3) << endl;//同上,从实践来看可以省略(*)cout << pa[0](av,3) << endl; //此处指向的是指针型函数,如果取内容需要加*cout << (*pb[1])(av,3) << endl;
- 运算符[]的优先级高于*
- auto只能用于单值初始化而不能用于初始化列表
// 使用real代替doubletypedef double real;// 将p_fun声明为函数指针类型的别名typedef const double *(*p_fun) (const double *, int);p_fun p1 = f1;p_fun pa[3] = { f1,f2,f3};p)fun (*pd)[3] = &pa;
- 使用typedef可减少输入量,让你在编写代码时不容易犯错,并让程序更容易理解。
- #define与typedef 的区别在于,前者只是简单的字符替换,后者为类型取别名
内联函数(编译器将相应的函数代码替换函数调用)无需跳到另一个位置处执行代码再调回来,因此内联函数的运行速度比常规函数稍快,但代价是需要占用更多内存。
由于函数调用机制的过程相当快,因此尽管节省了该过程的大部分时间,但节省的时间绝对值并不大。
要使用这一特性,必须采取的措施:
- 通常省略原型,将整个定义放在本应提供原型的地方。
- 内联函数不能递归
- C语言使用预处理语句#define来提供宏——内联函数的原始实现,宏不能按值传递
C和C++使用&符号来指示变量的地址,引用是使用别名的一种,它们指向相同的值和内存单元。
int rats;int &rodents = rats;
注意:引用必须在声明的时候进行初始化。 引用更接近const指针
- 如果实参与引用参数不匹配,C++将生成临时变量
double function(const double &ra);
常规变量和const变量都可视为左值,因为可通过地址访问它们。
右值引用 使用 && 来声明
将引用用于结构:引用非常适合用于结构和类,比值传递效率更高
void set_pc(free_throws & ft);// 如果不希望函数修改传入的结构,可使用constvoid set_pc(const free_throws & ft);
- 引用作为函数的返回值时,必须在定义函数时在函数名前加&,返回引用的函数实际上是被引用变量的别名
free_throws & accumulate(free_throws & target, const free_throws & source);
- 返回引用时,应避免返回函数终止时不再存在的内存单元引用。同样的,也应避免返回指向临时变量的指针。
基类引用可以指向派生类对象,而无需进行强制类型转换。
通过函数原型,可以设置函数参数的默认值。
int harpo(int n, int m=4, int j=5);
在设计类时可发现,通过使用默认参数,可减少要定义的析构函数、方法以及方法重载的数量。
注意:匹配函数时,并不区分const和非const变量,也无法识别值传递还是引用传递。
仅当函数基本执行相同的任务,但使用不同形式的数据时,才应采用函数重载。
函数模板是通用的函数描述,使用泛型来定义函数,即通用编程。
模板特性有时也称参数化类型。 需要多个对不同类型使用同一种算法的函数时,可使用模板。template//关键字template 和 typename 是必需的// template //关键字class和typename是等效的,C++98添加typename之前,C++使用关键字class来创建模板void Swap(AnyType &a, AnyType &b){ AnyType temp; temp = a; a = b; b = temp;}// 显式具体化template<>void Swap (job & , job &); //写法1void Swap(job & , job &); //写法2// 显式实例化template void Swap (int ,int);
- 函数模板不能缩短可执行程序,最终仍由两个独立函数定义,最终的代码不包含任何模板。
- 代码中包含函数模板本身并不会生成函数定义,它只是一个用于生成函数定义的方案。编译器使用模板为特定类型生成函数定义时,得到的是模板实例。模板并非函数定义,但使用int的模板实例是函数定义。
- 试图在同一文件中使用同一种类型的显式实例和显式具体化将出错。
- 声明中使用前缀template —— 显式实例化
- 声明中使用前缀template<> —— 显式具体化
编译器的选择:
int x;decltype(x) y;decltype(x + y) xpy;xpy = x + y;
注意,在未声明x、y的时候不能使用decltype定义x和y的组合类型
auto h(int x,float y) -> double;// ->double被称为后置返回类型,其中auto是一个占位符,表示后置返回类型提供的类型
templateauto gt(T1 x, T2 y) -> decltype(x+y){ //... return x + y;}
注意:不要将函数定义或变量声明放到头文件中
头文件常包括如下内容:
包含自己的头文件时,应使用【“”】而不是【<>】:
头文件管理:
在同一个文件中只能将同一个头文件包含一次
避免机制:
#ifndef COORDIN_H_#define COORDIN_H_//...#endif
注意这种方法并不能防止编译器将文件包含两次,而只是让它忽略除第一次包含之外的所有内容
类型 | 作用域 |
---|---|
自动变量 | 局部 |
静态变量 | 全局or局部(取决于被如何定义) |
函数原型作用域中使用的名称 | 只在包含参数列表的括号内可用 |
C++函数 | 整个类/名称空间(包括全局的),但不能是局部的 |
要创建链接性为外部的静态持续变量,必须在代码块的外部声明它;
要创建链接性为内部的静态持续变量,必须在代码块的外面声明它,并使用static限定符; 要创建没有链接性的静态持续变量,必须在代码块内声明它,并使用static限定符
未被初始化的静态变量的所有位都被设置为0,这种变量被称为零初始化的(zero-initialized)
注意,即使使用extern声明,但进行了初始化,这时也为定义。
如果要在多个文件中使用外部变量,只需要在一个文件中包含该变量的定义(单定义规则),但在使用该变量的其他所有文件中,都必须使用关键字extern声明它。
注意,单定义规则并非意味着不能有多个变量的名称相同,因为局部变量可以隐藏全局变量,如果使用全局变量可以添加作用域解析运算符(:😃,例如:::warming
使用全局变量易于访问数据,但会使得程序不可靠,全局变量可让多个函数使用同一个数据块,外部存储尤其适用于表示常量数据,因为这样可以使用关键字const来防止数据被修改。
静态持续性、内部链接性
如果一个文件定义了一个静态外部变量,其名称与另一个文件中声明的常规外部变量相同,则在该文件中,静态变量将隐藏常规外部变量。静态存储连续性、无链接性
无链接性的局部变量,使用static限定符在代码块中定义,虽然变量只在该代码块中可用,但在代码块不处于活动状态时仍然存在。注意,如果初始化了静态局部变量,则程序只在启动时进行一次初始化,以后再调用函数时,将不会像自动变量那样再次被初始化。
在默认情况下全局变量的链接性为外部的,但const全局变量的链接性为内部的。只有未使用extern关键字的声明才能进行初始化。
动态内存有运算符new和delete控制,而不是由作用域和链接性规则控制。
通常,编译器使用三块独立的内存:一块用于静态变量(可能再细分),一块用于自动变量,另一块用于动态存储。
存储方案和动态分配
① 使用new运算符初始化:int *pi = new int (6); //设置*pi为6
struct where {double x; double y; double z;};
初始化常规结构或数组:where * one = new where {2.5,5.3,7.2};
② new失败时:引发异常std::bad_alloc
③ new 运算符、函数和替换函数:
// 分配函数,位于全局名称空间中void * operator new(std::size_t);void * operator new[](std::size_t);//std::size_t是一个typedef,对应于合适的整型// 释放函数void operator delete(void *);void operator delete[](void *);
int * pi = new int;//被转换为:int * pi = new(sizeof(int));int * pa = new int[40];//被转换为:int * pa = new(40 * sizeof(int));delete pi;//被转换为:delete(pi);
定位new运算符:能够指定要使用的位置
需要包含头文件new:
#include<new>
,使用定位new运算符时,变量后面可以有方括号,也可以没有。
#includestruct chaff{ char dross[20]; int slag;};char buffer1[50];char buffer2[500];int main(){ chaff *p1, *p2; int *p3, *p4; p1 = new chaff; p3 = new int[20]; p2 = new (buffer1) chaff; //从buffer1中分配空间给结构chaff p4 = new (buffer2) int [20];//从buffer2中分配空间给一个包含20个元素的int数组 //.......}
定位new运算符的另一种用法是,将其与初始化结合使用,从而将信息放在特定的硬件地址处
定位new运算符的工作原理:基本上,它只是返回传递给它的地址,并将其强制转换为void *,以便能够赋给任何指针类型。注意这里说的是默认定位new函数,C++允许程序员重载定位new函数。
定位new函数不可替换,但可重载,它至少需要接收两个参数,其中第一个总是std::size_t,指定了请求的字节数。这样的重载函数都被称为定义new,即使额外的参数没有指定位置。
在C++中,名称可以是变量、函数、结构、枚举、类以及类和结构的成员。
注意,变量并非在其潜在作用域内的任何位置都是可见的。
命名空间可以是全局的,也可以位于另一个名称空间中,但不能位于代码块中。因此在默认情况下,在名称空间中声明的名称的链接性为外部的(除非它引用了常量)。
全局名称空间(global namespace):对应于文件级声明区域
名称空间是open的,可以把名称加入到已有的名称空间中。
命名空间可以嵌套
namespace Jill{ double bucket(double n){ ...} double fetch; int pal; struct Hill{ ...};}namespace Jill{ char * goose(const char *);}namespace Jill{ double bucket(double n) { ..... }}Jill::fetch = 12.34;Jill::Hill mole;Jill::bucket(double n);
原来的Jill命名空间为bucket()函数提供了原型。可以在该文件后面(或另外一个文件中)再次使用Jill命名空间来提供该函数的代码
using Jill::fetch;
注意,如果在局部声明,则为局部变量;如果在全局声明,则为全局变量
using namespace Jill;
假设名称空间为全局的,如果使用using编译指令导入一个已经在函数中声明的名称,则局部名称将隐藏名称空间名,就像隐藏同名的全局变量一样。
一般来说,使用using声明比使用using编译指令更安全
namespace elements{ namespace fire { int flame; .... } float water;}using namespace elements::fire;
也可以在名称空间中使用using编译指令和using声明
namespace myth{ using Jill::fetch; using namespace elements; using std::cout; using std::cin;}std::cin >> myth::fetch;std::cout << Jill::fetch;//如果没有与之冲突的局部变量using namespace myth;cin >> fetch;
using编译指令是可传递的
namespace my_very_favorite_things { ...};namespace mvft = my_very_favoritte_things;namespace MEF = myth::elements::fire;using MEF::flame;
未命名的名称空间:可通过省略名称空间的名称来创建未命名的名称空间
namespace{ int ice; int bandycoot;}
未命名的名称空间,像后面跟着using 编译指令一样,潜在作用域:从声明点到该声明区域末尾。这提供了链接性为内部的静态变量的替代品。
注意:在名称空间中声明的函数名的作用域为整个名称空间,因此定义和声明必须位于同一个名称空间中。
使用名称空间的主旨是简化大型编程项目的管理工作
最重要的OOP特性:
类是一种将抽象转换为用户定义类型的C++工具,它将数据表示和操纵数据的方法组合成一个整洁的包。
一般来说,类规范由两个部分组成:
要使用类,必须了解其公共接口;要编写类,必须创建其公共接口
访问控制:
OOP是一种编程风格,从某种程度来说,它可用于任何一种语言。
- 首先,将数据表示和函数原型放在一个类声明中(而不是放在一个文件中),通过将所有内容放在一个类声明中,来描述称为一个整体。
- 其次,让数据表示称为私有,使得数据只能被授权的函数访问。 数据隐藏不仅可以防止直接访问数据,还让开发者(类的用户)无需了解数据是如何被表示的。
类和结构:
类描述看上去很像是包含成员函数以及public和provate可见性标签的结构声明。它们之间唯一的区别是,结构的默认访问类型是public,而类为private。
实现类成员函数
==定义位于类声明中的函数都将自动成为内联函数,==类声明常将短小的成员函数作为内联函数。
如果愿意,也可以在类声明之外定义成员函数,并使其成为内联函数,此时只需在类实现部分中定义函数时使用inline限定符
class Stock{ private: void set_tot()};inline void Stock::set_tot(){ ///}//对象的创建Stock A;Stock B=Stock(10,20.0);Stock C(10,20.0);Stock D=Stock(1,2.2);A = D; A = Stock(11,22.2);Stock E={ 10,20.0};Stock F{ 10,20.0};Stock *G = new Stock(10,20.0);Stock *H = new Stock{ 10,20.0};
- 确保内联定义对多文件程序中的所有文件都可用的最简便方法是:将内联定义放在定义类的头文件中。
- 根据改写规则(rewrite rule),在类声明中定义方法等同于用原型替换方法定义,然后在类声明的后面将定义改写为内联函数。
注意,在OOP中,对象使用方法时,执行的是同一个代码块,只是将这些代码用于不同的数据。调用成员函数被称为发送消息,因此将同样的消息发送给两个不同的对象将调用同一个方法,但该方法被用于两个不同的对象。
- new创建类对象需要指针接收,一处初始化,多处使用
- new创建类对象使用完需delete销毁
- new创建对象直接使用堆空间,而局部不用new定义类对象则使用栈空间
- new对象指针用途广泛,比如作为函数返回值、函数参数等
- 频繁调用场合并不适合new,就像new申请和释放内存一样
- 【https://blog.csdn.net/adam_tu/article/details/8775190】
使数据成为公有,违背了类的一个主要初衷:数据隐藏
注意:构造函数的参数表示的不是类成员,而是赋给类成员的值。因此,参数名不能与类成员相同。为避免这种混乱,两种常见的做法:①在数据成员名中使用m_
前缀;②在成员名中使用后缀_
;
Stock food = Stock("world Cabbage",250,1.25);
Stock garment("Furry Mason",50,2.5);
Stock *pstock = new Stock("Electroshock Games",18,19.0);
如果没有提供任何构造函数,则C++将自动提供默认构造函数,它是默认构造函数的隐式版本,不做任何工作。
当且仅当没有定义任何构造函数时,编译器才会提供默认构造函数。如果提供了带参数列表的非默认构造函数,在实例化的时候没有指定参数,则声明会报错。
给已有构造函数的所有参数提供默认值
Stock(const string & co="Error",int n=0,double pr=0.0);
通过函数重载来定义另一个构造函数,Stock();
Stock::Stock(){ company = "no name"; shares = 0; share_val = 0.0; total_val = 0.0;}
==注意:由于只能有一个默认构造函数,因此不能同时采用以上这两种方式。==因为这会出现二义性
在设计类时,通常应提供对所有类成员做隐式初始化的默认构造函数。隐式调用默认构造函数,不使用圆括号。
默认构造函数可以没有任何参数,如果有,则必须给所有参数都提供一个默认值。
区分:
Stock first("Concrete Conglomerate"); //调用非默认构造函数Stock sencond(); // 声明一个返回Stock对象的函数Stock third; // 隐式调用默认构造函数,不使用圆括号
{ public: ~Stock();};Stock::~Stock(){ /}
- 如果创建的是静态存储类对象,则其构造函数将在程序结束时自动被调用
- 如果创建的是自动存储类对象,则其析构函数将在程序执行完代码块时(该对象是在其中定义的)自动被调用
- 如果对象是通过new创建的,则它将驻留在栈内存或自由存储区中,当使用delete来释放内存时,其析构函数将自动被调用
如果程序员没有提供析构函数,编译器将隐式地声明一个默认析构函数,并在发现导致对象被删除的代码后,提供默认析构函数的定义。
注意:在默认情况下,将一个对象赋给同类型的另一个对象时,C++将源对象的每个数据成员的内容复制到目标对象中相应的数据成员中。
stock1 = Stock("Nifty Foods",10,50.0);
,值得注意的是,这条语句不是对stock1进行初始化,而是赋新值给它,这是通过让构造程序创建一个新的、临时的对象,然后将其内容复制给stock1来实现的。随后程序调用析构函数,以删除该临时对象。自动变量被放在栈中,∴最后创建的对象最先删除,最先创建的对象最后删除
如果既可以通过初始化,也可通过赋值来设置对象的值,则应采用初始化方式。通常这种方式的效率更高。
const Stock land = Stock("Kludgehorn");land.show() // 该句将会报错,因为show()无法确保调用对象不被修改
如果要确保函数不会修改调用对象,C++的解决方法是将const关键字放在函数的括号后面:
void show() const;//声明void Stock::show() const //定义{ /}
只要类方法不修改调用对象,就应将其声明为const
要让程序知道存储的数据,最直接的方式是让方法返回一个值。为此,通常使用内联代码:
const Stock & topval(const Stock & s) const;
,该函数隐式的访问一个对象,而显式地访问另一个对象,并返回其中一个对象的引用。括号中的const表明,该函数不会修改被显式地访问的对象;而括号后的const声明,该函数不会修改被隐式地访问的对象。由于该函数返回了两个const对象之一的引用,因此返回类型也应为const引用。隐式访问,访问的是这个函数所在的对象;显式访问,访问的是该函数的参数对象
this指针指向用来调用成员函数的对象(this被作为隐藏参数传递给方法)。一般来说,所有的类方法都将this指针设置为调用它的对象的地址
注意:每个成员函数(包括构造函数和析构函数)都有一个this指针。this指针指向调用对象。如果方法需要引用整个调用对象,则可使用表达式==*this==。在函数的括号后面使用const限定符将this限定为const,这样将不能使用this来修改对象的值。再强调一遍,this是指向对象的指针,是对象的地址,对于对象本身则为【*this】
Stock mystuff[4];mystuff[0].update();mystuff[4].show();const Stock * tops = mystuff[2].topval(mystuff[1]);
可以用构造函数来初始化数组元素,这种情况下必须为每个元素调用构造函数,用括号括起来的、以逗号分隔的值列表:
const int STKS = 4;Stock stocks[STKS] = { Stock("Nano", 12.5, 20), Stock("Boff", 200, 2.0), Stock(), Stock(),};
初始化对象数组的方案是,首先使用默认构造函数创建数组元素,然后花括号中的构造函数将创建临时对象,然后将临时对象的内容复制到相应的元素中。因此,要创建类对象数组,则这个类必须有默认构造函数。
在类中定义的名称的作用域为整个类,作用域为整个类的名称只在该类中是已知的,在类外是不可知的。因此,可以在不同类中使用相同的类成员名而不会引起冲突。此外,类作用域意味着不能从外部直接访问类的成员,公有函数也是如此,即调用公有成员函数必须通过对象。
在定义成员函数时,必须使用作用域解析运算符。
在使用类成员名时,必须根据上下文使用直接成员运算符.
、间接成员运算符->
或作用域解析运算符::
Ik * pik = new Ik;Ik ee = Ik(8);ee.ViewIk();pik->ViewIk();
class Test{ private: enum { Months = 12}; //用这种方式声明枚举并不会创建类数据成员,即所有对象中都不包含枚举 double costs[Months];};
class Test{ private: static const int Months = 12; //该常量将与其他静态变量存储在一起,而不是存储在对象中。 double costs[Months];};
传统枚举存在的问题:两个枚举定义中的枚举量可能发生冲突,这将无法通过编译。
C++提供了一种新枚举,其枚举量的作用域为类,声明如下:
enum class egg { Small,Medium,Large,Jumbo};enum struct t_shirt { Small,Medium,Large,Xlarge};egg choice = egg::Large;t_shirt Floyd = t_shirt::Large;
注意:常规枚举可以自动转换为整型(如将其赋给int变量或用于比较表达式),但作用域内枚举不能隐式地转换为整型,在必要时可进行显式类型转换:
int Frodo = int(t_shirt::Small);
默认情况下,C++作用域内枚举的底层类型为int
,此外还可指定为其他底层类型,底层类型必须为整型,如果没有指定,编译器选择的底层类型将随实现而异。
enum class : short { Small,Medium,Large,Jumbo};
栈的特征:首先栈存储了多个数据项,其次栈由可对它执行的操作来描述。
类概念非常适合于ADT方法
私有部分必须表明数据存储的方式,公有接口应隐藏数据表示而以通用的术语来表达。
正如C++创始人Bjarne Stroustrup在一次专业程序员大会上所建议的:“轻松地使用这种语言。不要觉得必须使用所有的特性,不要在第一次学习时就试图使用所有的特性。”
声明格式如下:
operator op(argument-list)
operator+()
重载+运算符
operator*()
重载*运算符 注意,op必须是有效的C++运算符
将参数声明为引用的目的是为了提高效率(速度更快,使用的内存更少),返回类型不能是引用
不要返回指向局部变量或临时对象的引用。函数执行完毕后,局部变量和临时对象将消失,引用将指向不存在的数据
- 重载后的运算符必须至少有一个操作数是用户定义的类型,这将防止用户为标准类型重载运算符
- 使用运算符时不能违反运算符原来的句法规则,也不能修改运算符的优先级
- 不能创建新的运算符
- 不能重载以下运算符:【sizeof】、成员运算符【.】、成员指针运算符【.*】、作用域解析运算符【::】、条件运算符【?:】、RTTI运算符【typeid】、强制类型转换运算符【const_cast】、强制类型转换运算符【dynamic_cast】、强制类型转换运算符【reinterpret_cast】、强制类型转换运算符【static_cast】
- 表中大多数运算符都可以通过成员或非成员函数进行重载,但以下运算符只能通过成员函数进行重载:赋值运算符【=】、函数调用运算符【()】、下标运算符【[]】、指针访问类成员的运算符【->】
表示法中没有任何内容可以表明运算符完成的工作,因此最好定义一个其名称具有说明性的类的方法
友元有3种:友元函数、友元类、友元成员函数
friend
friend Time operator*(double m,const Time & t);
注意:
- 虽然
operator*()
是在类声明中声明的,但它不是成员函数,因此不能使用成员运算符来调用- 虽然
operator*()
不是成员函数,但它与成员函数的访问权限相同
step2:编写函数定义
注意因为它不是成员函数,所以不要使用
Time::
限定符,另外不要在定义中使用关键字friend
只有类声明可以决定哪一个函数是友元,因此类声明仍然控制了哪些函数可以访问私有数据
// 该函数只适于A = B * 2.75的情况Time Time::operator*(double mult) const{ Time result; long totalminutes = hours * mult * 60 + minutes * mult; result.hours = totalminutes / 60; result.minutes = totalminutes % 60; return result;}// 要想其具有交换律//法1:函数重载Time Time::operator+(double m,const Time & t){ return t * m;}//法2:友元函数class A{ public: friend Time operator*(double m,const Time & t);};Time operator*(double m,const Time & t){ Time result; long totalminutes = t.hours * m * 60 + t.minutes * m; result.hours = totalminutes / 60; result.minutes = totalminutes % 60; return result;}
如果要为类重载运算符,并将非类的项作为其第一个操作数,则可以用友元函数来反转操作数的顺序。
void operator<<(ostream & os, const Time & t){ os << t.hours <<"hours, "<<<"minutes";}//
注意,以上方式只能实现cout<<trip;
的形式,对于cout<<"test:"<<trip;
的形式
ostream & operator<<(ostream & os, const Time & t){ os << t.hours <<"hours, "<< t.minutes<<" minutes"; return os;}
当执行cout<<trip;
时,将被转换为operator<<(cout,trip);
,该调用返回cout对象,因此cout<<"test:"<<trip;
是可以执行的。
一般来说,要重载<<运算符来显式c_name的对象,可使用一个友元函数,其定义如下:
ostream & operator<<(ostream & os, const c_name & obj){ os<<......; return os;}
只有在类声明中的原型中才能使用
friend
关键字,除非函数定义也是原型,否则不能在函数定义中使用该关键字。
一般来说,非成员函数应是友元函数,这样它才能直接访问类的私有数据。
//成员函数Time operator+(const Time & t) const;//友元函数friend Time operator+(const Time & t1,const Time & t2);
- 对于成员函数版本来说,一个操作数通过this指针隐式地传递,另一个操作数作为函数参数显式地传递;
- 对于友元版本来说,两个操作数都作为参数来传递
非成员版本的重载运算符函数所需的形参数目与运算符使用的操作数数目相同;而成员版本所需的参数数目少一个,因为其中的一个操作数是被隐式地传递的调用对象。
T1 = T2 + T3;//成员函数版本:T1 = T2.operator+(T3);//友元版本T1 = operator+(T2,T3);
OOP传统,即将类接口的重点放在其本质上(抽象模型),而隐藏细节。
在C++中,接受一个参数的构造函数为将类型与该参数相同的值转换为类提供了蓝图。
只有接受一个参数的构造函数才能作为转换函数。
有时利用构造函数用作自动类型转换会导致意外的类型转换,因此可用关键字explicit
来关闭这种自动特性,声明构造函数时可按如下声明:explicit Stonewt(double lbs);