c++学习笔记(网上资料)
C++笔记 2007-3-22
1、 程序 —— 可执行文件,人发送给计算机的一组指令。 硬件指令是二进制,方便与数字电路和指令集转换。
2、 我们学的是符合ANSI规范的标准C++,有可移植性
3、 进制:十进制 -> 二进制:除2取余 88 -> 0101 1000 二进制 -> 十进制 : 各个位与权重的乘机,之后的总和 0101 1000 -> 8+16+64 -> 88 十进制 -> 十六进制 :除16取余 88 -> 0x58
4、 1byte =8 bit 1 kb = 1024 byte 1M = 1024 kb
5、 开发周期 源程序 --> 可执行代码 --à> 可执行程序 --à> 调试 --à>à 运行 (1) (2) (1)编译 :生成自己的可执行代码(缺少库函数代码) (2)连接 : 与系统的可执行代码合到一起 源文件后缀 :”.cc”,”.cp”,”.cpp”
6、第一个C++程序 #include < iostream > ----“#”表示预编译选项,<>为一个头文件,其中有常用函数 using namespace std; ----命名空间,用到的全局变量都是个空间的
int main(){ ----主函数,每个C++程序都必须要有的函数,是程序执行的入口,main函数结束则程序结束 cout << "Hello World" << endl; ----“cout”是一个全局变量,代表标准输出设备;“<<”输出运算符,输出到标准输出设备上;"endl"换行符,与”\n“效果相同 return 0; }
% g++ -c hello.cc ----编译生成目标文件hello.o % g++ hello.o ----连接生成可执行文件 % g++ -o hello hello.o ----连接生成可执行文件并指定可执行文件名字及生成目录(可执行文件默认为a.out) % a.out ----执行可执行文件
% g++ hello.cc ----直接生成可执行文件(目标文件hello.o在内存中生成) * 运行可执行文件要在.bash_profile文件中配制PATH环境变量,加上当前路径“.”,用“:”分隔
7、注释 多行注释 /* ... */ 单行注释 //
8、头文件 #include < > 系统头文件 #include " " 自定义的头文件
9、变量 用于存储数据,每个变量都要有类型和名字 使用变量:(1)声明 :确定变量类型及名字;(2)赋值,第一次的赋值称为初始化 用双引号保存的是字符串,用单引号保存的是字符 计算机保存字符,是保存的字符对应的ASCII码。 对字符的声明 char c ; c='a'; 对字符数组的声明 char name[]="Mary"; ---字符数组用“=”赋值,必须在初始化时候
C++笔记(day02) 2007-3-23
1、%ldd 可行文件的名字; ---可查看连接库
2、C++严格区分大小写,变量命名只能使用字母,数字,或下划线,第一个字母必须使用字母或下划线
3、float单精度,double双精度
sizeof(int)---返回int在内存中占的大小
size(bool)=1
size(char)=1
size(short)=2
size(int)=4
size(long)=4
size(float)=4
size(double)=8
无符号的数据一旦溢出,将成为0。
有符号的int最大值2147483647,益处变为最小值,负数
4、不同数据间赋值:有兼容性的,占空间小的数据类型,可以给占空间大的数据类型赋值
不兼容的,可能会造成数据丢失。int ->float可以转换;float->int 小数部分丢失
5、 运算符
要求数据类型要相同
i++ 先用后加
++i 先加后用
布尔运算符号:结果真或假
逻辑运算符:做bool运算!(1)“与”运算&&,两条件要同时成立(2)“或”运算||,只要有一个条件是真,就成立
6、按位与“&” 有0则与的结果为0
按位或“|” 有1则或的结果为1
按位异或“^” 两位不同则为1,相同为0
7、左移 “<<" 左移1相当于乘以2
右移 “>>" 右移1相当于除以2
8、三目运算符
条件?真:假
9、返回变量 --- 表达式可以再次赋值,赋值语句返回变量本身 eg:(a=10)=20
返回变量的值 --- 不可再次赋值 eg:(a+5)=10
左值:变量、赋值表达式,++i的返回值是左值
右值:不能放在“=”左边的是右值,i++的返回值是右值
常量要在声明时就赋值,并且不能修改
10、流程控制
while(条件){循环体} 条件成立,则进入循环,知道条件为假时,退出循环
(1)声明循环变量
(2)while()确定循环条件
(3)循环变量要有所变化,避免形成死循环
for(变量初始化;条件;变量的变化){循环体代码}
开始时判断循环条件是否成立,循环结束后,到第三个条件,使用变量改变
do{循环体}while(条件);
先执行一次,再判断条件,选择是否继续执行循环
break; 跳出循环,执行循环之外的下一条语句
continue; 从次处开始,结束本次循环,进行下一次循环
11、课堂练习---求1到100之间的所有素数
C++笔记 (day03) 2007-03-26
1、cin 输入流一旦破坏,就不能恢复
2、函数:一段相关代码组成的集合,以便完成一个特定的目的。
(1)可以通过名字来使用,曾强了代码的可重用性。
(2)提高可维护性。
3、函数定义=声明+函数体:
<返回类型> 函数名(<参数表>...){
return ...
}
函数返回值:只有在调用函数的同时才能保存返回值
函数定义时,小括号里的参数称为形参,在函数被调用的时候才有意义。
函数调用时,小括号里的被传入的参数称为实参。
4、函数在调用之前只有声明就可以,因为在编译阶段,参照声明可以完成语法上的检测。
函数声明 <返回类型> 函数名(<参数表>...);
声明与定义的分离,实现并行开发!
5、全局变量,局部变量
局部变量:函数内部声明的变量,只能在它声明的函数内部使用。如果没有对其初始化就使用,则其值为垃圾数字。
全局变量:能被所有的函数共同使用,要声明在所有函数(包括main函数)之外。尽量少使用,影响太大。如果没有对其初始化就使用,系统默认为其初始化为0。
6、数据区:只存放全局数据,数据区的数据是稳定的,程序启动时,即创建,程序结束时,才消失
堆区
代码区
栈区:存储局部变量。插入数据从栈顶插入,先进后出结构。
如果函数不调用的话,局部变量是不存在的
在main函数与其他函数之间有个标记,一旦函数返回,栈顶下落到标记处
栈中的数据是变化频繁的
函数调用时创建,函数返回即消失
7、 值传递
函数调用并不是传递实参本身,而是创建一个临时变量传到函数中
(1)为形参创建一个临时变量,临时变量的值是拷贝实参的值
(2)对形参的使用,实际上是对临时变量的使用,不会改变实参的大小
8、默认参数
(1)area(float a , float b =10); 当只传递一个参数时,形参b的位置默认为10
(2)有默认值的形参靠右摆放
9、内联函数
声明时加“inline”,运行效率高,不支持结构控制语句,必须先定义,不支持函数原形。
10、递归函数
在函数内部自己调用自己。
必须有明确的返回条件----递归的出口
11、函数重载
(1)函数名相同。
(2)参数表不同(参数个数不同,参数类型不同,参数顺序不同)。
(3)与参数名和返回类型不关
调用时会根据参数表匹配自动调用相应的函数
编译之后,函数的名字都会发生改变,后面会加上形参缩写作为后缀,没有任何函数的名字是一样的
不会做自动类型提升
对重载的函数最好不使用参数默认值
12、const
在函数内部不允许修改此形参
13、需求分析、设计、代码实现
多文件示例:
=========================================
chang.h
=========================================
char change( char );
=========================================
chang_impl.cc
=========================================
char change( char c ){
if( c >= 97 && c <= 122 ){
return c - 32 ;
}else if( c >= 65 && c <= 90 ){
return c + 32 ;
}else{
return c ;
}
}
=========================================
main.cc
=========================================
#include <iostream>
#include "change.h"
using namespace std;
int main(){
char c = '0' ;
while( 1 ){
cout<<"enter a char [ 0 exit] >";
cin>>c ;
if( c == '0' ){
break;
}
cout<<" ====> " << change( c ) <<endl;
}
return 0 ;
}
===========================================
14、外部变量和内部变量
外部变量:extern int g; ----声明外部变量,引入一个变量,在别的源程序中声明
内部变量:在源文件中声明的变量
外部变量肯定是一个全局变量!
作用域 生命周期 存储位置
全局变量(外部变量) 所有函数 程序启动->程序结束 数据区 Data
局部变量 函数内部 函数开始调用->函数返回 栈 Stack
静态局部 函数中 第一次调用函数时(只被初始化一次)->程序结束 数据区 ---只需要一份数据,并且不希望别的函数访问他
静态全局 本源文件中 程序开始->程序结束 数据区
作业:实现银行业务 ========= a.cc ==================
long id;
int password;
double balance;
long(id) create(balance , password); //创建账户
void save( sum );
int(staus 0,-1) withdraw(sum , password);
float query();
===================================
generatorId(); //得到唯一的ID号
int showMenu(); //现实主菜单
saveMenu(); //收集开户信息,然后调用创建账户
createMenu();
withdrawMenu();
queryMenu();
===================================
***今日重点:(1)形参和实参的区别
(2)递归
(3)函数重载
练习: 观察费波拉切数列1,1,2,3,5,8,13,21,34. . . . . .第一项和第二项都是1,后面的每一项是前面相邻两项的和。
使用递归方法计算第n项的费波拉切数列结果。
C++笔记 2007-03-27
1、程序由函数组成,函数只完成自己特定的功能即可
把函数声明写在头文件里(想使用函数时,可直接导入头文件,调用函数),把函数实现写在".cc"文件中
把多个".cc"文件编译成可执行文件 ->分别编译成".o"文件,再连接到一起
2、值传递
函数中的参数传递是值传递,形参只是实参的一份拷贝数据,在函数中改变形参的值,对实参无影响
3、作业分析:显示层(与用户的交互)
操作数据(完成业务逻辑) biz层
数据(id , password , balance )
Bank实现代码
================================================================
biz.cc
================================================================
//operation
/* p : Password of account .
* b : balance of account .
* return : id of account .
*/
long create( int p , double b );
void save( double sum ) ;
/*
* return : 0 success , otherwise -1 returned .
*/
int withdraw( int p , double sum ) ;
double query( int p ) ;
long generateId();
================================================================
biz.cc
================================================================
static long id ;
static int passwd ;
static double balance ;
#include <iostream>
using namespace std;
long generateId(){
static int id = 1 ;
return id++ ;
}
long create( int p , double b ){
id = generateId();
passwd = p ;
balance = b ;
return id ;
}
void save( double sum ){
balance += sum ;
}
int withdraw( int p , double sum ){
if( p != passwd ){
cout<<"invalid password ." << endl;
return -1 ;
}
if( balance < (sum + 10) ){
cout<<"no enough money ." << endl;
return -1 ;
}
balance -= sum ;
return 0 ;
}
double query( int p ){
if( p != passwd ){
cout<<"invalid password " << endl;
return -1 ;
}else{
return balance ;
}
}
================================================================
menu.h
================================================================
int showMenu();
void createMenu();
void saveMenu();
void withdrawMenu();
void queryMenu();
================================================================
menu.cc
================================================================
#include "biz.h"
#include <iostream>
using namespace std;
int showMenu(){
cout<<"create ------> 1 " << endl;
cout<<"save ------> 2 " << endl;
cout<<"withdraw ----> 3 " << endl;
cout<<"query -------> 4 " << endl;
cout<<"exit --------> 0 " << endl;
cout<<"enter your choice >";
int c ;
cin>>c ;
if( !cin ){
return -1 ;
}else{
return c ;
}
}
void createMenu(){
int passwd ;
double balance ;
cout<<"\tenter password >";
cin>>passwd ;
cout<<"\tenter balance >";
cin>>balance ;
long id = create( passwd , balance );
cout<<"======================"<<endl;
cout<<"create account ok , id = " << id <<endl;
cout<<"======================"<<endl;
}
void saveMenu(){
double sum ;
cout<<"\tenter sum >";
cin>>sum ;
save( sum ) ;
cout<<"======================"<<endl;
cout<<"save money ok "<<endl;
cout<<"======================"<<endl;
}
void withdrawMenu(){
int passwd ;
double sum ;
cout<<"\tenter password >";
cin>>passwd ;
cout<<"\tenter sum >";
cin>>sum ;
int ret = withdraw( passwd , sum ) ;
if( ret == 0 ){
cout<<"========================"<<endl;
cout<<"withdraw successful . "<<endl;
cout<<"========================"<<endl;
}
}
void queryMenu(){
int passwd ;
cout<<"\tenter password >";
cin>>passwd ;
double ret = query( passwd ) ;
if( ret != -1 ){
cout<<"============================"<<endl;
cout<<"BLANCE : $ " << ret << endl;
cout<<"============================"<<endl;
}else{
cout<<"============================"<<endl;
cout<<"invalid password "<<endl;
cout<<"============================"<<endl;
}
}
================================================================
main.cc
================================================================
#include <iostream>
#include "menu.h"
using namespace std;
int main(){
int c = 0 ;
do{
c = showMenu();
if( c == -1 ) { break ; }
switch( c ){
case 1 :
createMenu();
break;
case 2 :
saveMenu();
break;
case 3 :
withdrawMenu();
break;
case 4 :
queryMenu();
break;
case 0 :
cout<<"===================="<<endl;
cout<<"Good Bye "<<endl;
cout<<"===================="<<endl;
break;
default :
cout<<"===================="<<endl;
cout<<"invalid option, try again.";
cout<<endl;
cout<<"===================="<<endl;
break;
}
}while( c != 0 );
return 0 ;
}
================================================================
4、数组
(1)声明数组 <元素类型> 数组名[元素个数] int intArray[100]; -->intArray 是个集合,有100个元素,每个元素都是int类型的
(2)初始化数组
(3)使用 通过数组的下标来访问数组中的元素,下标从0开始 intArray[0]=100; -->intArray数组中的第一个元素赋值为100
数组声明时,元素个数必须是常量表达式
数组声明带有初始化,则可直接为数组赋值
在数组声明时,必须指明数组长度,若在声明时候初始化,数组长度可省
int a1[2]={100,200}; 长度2
int a2[] = {5,6,7}; 长度3
对于数组中的元素个数,比声明时的多,则会报错,这样的越界访问,对整个程序来说会有很严重的后果!!!
比声明少,系统会把余下的没有定义数据的元素初始化为0
不初始化的数组,其中的元素数据为随机数
下标工作的原理:
表示编号,还表示当前元素相对于数组起始位置的偏移量
计算机通过偏移量找到元素在内存中的位置
5、数组的排序
选择排序:找出最小的放在第一个位置
数组元素有n个,需要找n-1次,需要比较n-i次(i为开始元素的位置)
6、多维数组
二维数组;一个数组中的每个元素是个数组
声明: int iA[5][10]; -->“5”代表数组有5行,“10”代表数组有10列
声明时,第一维可以省略!
确定一个元素需要2个下标
7、结构
用户自己定义的一组数据类型
声明结构时,编译器不会分配空间,在声明一个结构的变量的时候才会为其分配空间
结构中的成员是多个简单类型组成的
用 “结构名.成员名”来操作其中的成员变量
strcpy(p.name,"huxz");为char数组赋值
结构类型的变量间也可以相互赋值
结构的大小就是所有成员的大小之和(每个成员的大小必须是int类型的整倍数,当不够时,会自动补齐)
Unix上还要求,结构的大小是结构的最大的成员的整倍数
size(of) 计算结构大小
作业:把银行系统用结构改写,要求可以开多个账户(定义一个account类型的数组保存)
struct account{
long id;
int password;
double balance;
}
create(int password,double balance);
void save(int id , double sum);
int withdraw(int id,int password,double sun);
double query(int id,int password);
C++笔记 2007年3月28日
1、变量的存储
(1)内存是一块空间,把其中的每个字节做了编号,为了以后计算机能通过编号找到数据
(2)编址方式:绝对编址(在整个程序中使用),相对编址(字节相对于逻辑0偏移量,在进程中使用)
2、取变量地址
(1)"&" &i 表示取内存中i的地址
地址的编址用十六进制表示
(2)逻辑0在代码区
全局变量在数据区,地址的编址是大于0的
局部变量在栈区,地址的编址是小于0的
3、数组、结构的地址
(1)数组中的数据在内存中是连续存储的。 数组中每个元素的地址相差的值应为数组元素类型的大小。
(2)结构的地址:
结构的空间是连续的。
结构的起始地址与第一个成员变量的地址是一样的。
4、存储地址—
指针:存储变量的地址
指针的类型由将要保存的地址的变量类型决定
int*只能保存int变量的地址
指针赋值一定要是同类型的指针才能相互赋值!
5、指针的运算
(1)指针和指针之间的运算
“+”,“*”,“/” 指针与指针间是不能做这些运算,没有意义!
“-” 可以做减法运算,以“sizeof(指针类型)”作为计算单位的! 注意:要同类型的指针才能做此运算,不同的话,会对运算单位产生歧义。
(2)指针和数字之间的运算(加、减都可以)
int i = 100;
int * p = &i;
打印 p+1 -> 相当于在地址上加4,因为存储的变量是int类型的
p+2 -> 相当于在地址上加8
6、通过指针访问所指向的变量
*p 代表指针p所指向的变量 *p <=> i
指针在声明的时候,即初始化
int * p = NULL; 表示没有明确指向,不能 *p ,会出现 “段错误”的异常 -->空指针
段错误原因 (1)空指针
(2)数组越界
(3)递归的条件不正确
7、课堂练习
用指针打印出数组中个元素的值
#include <iostream>
using namespace std;
int main(){
int ai[6]={34,4,12,67,34,2};
int *p = &ai[0];
for(int i = 0 ; i < 6 ; i++){
cout <<"a[" << i << "]=" <<*(p+i) << endl;
}
return 0;
}
int *p = ai ; 数组的本质就是用指针实现的,数组的名字就代表数组的首地址(起始地址)
数组的名字是指向数组首地址(a[0])的指针
ai 数组名,就是指向数组首地址的指针,可以用下标取元素,也可以把数组名当指针来访问元素 *(ai+n)
p 指针名,也是指向首地址的指针,也可以通过下标(像数组名一样)访问数组元素
p[n] <=> *(p+n)
8、结构指针
struct person{
int id;
int age;
}
int main(){
person per = {1,20};
person* p = &per;
cout << "per.id ="<<per.id<<endl; //通过结构名取成员变量
cout << "per.age=" << per.age <<endl;
cout <<"======================"<<endl;
cout << "(*P).id=" << (*P).id <<endl; //通过指针访问结构的成员变量
cout << "(*P).age=" << (*P).age <<endl; // (*p).id <=> p->id 只有结构指针可以这样使用
cout <<"======================"<<endl;
cout << "p->id=" << p->id <<endl;
cout << "p->age=" << p->age <<endl;
return 0;
}
9、指针的地址
指针变量在内存中占4个字节(与类型无关,因为保存地址的指针只保存地址)
保存int型指针(int* p = &i)的地址用int**保存(int** pp = &p)
#include <iostream>
using namespace std;
int main(){
int i = 0 ;
int * p = &i ;
int ** pp = & p ;
cout<<"&i = " << &i << endl;
cout<<"p = " << p << endl;
cout<<"&p = " << &p << endl;
cout<<"pp = " << pp << endl;
cout<<"&pp = " << &pp << endl;
cout<<"i = " <<i << endl;
cout<<"*p = " << *p << endl;
cout<<"*pp = " << *pp << endl;
cout<<"**pp = " << **pp << endl;
return 0 ;
}
执行结果:
&i = 0xffbffbec
p = 0xffbffbec
&p = 0xffbfbe8
pp = 0xffbfbe8
&pp = 0xffbffbe4
i = 0
*p = 0
*pp = 0xffbffbec
**pp = 0
pp -> p -> i 指向关系
pp=&p p=&i
*pp=p *p=i **pp=*p=i
C++笔记 2007年3月29日
1、数组指针声明的时候不用初始化,声明以后就指向数组的首地址了,以后不允许改变,所以,数组指针可以认为是一个常量,一旦赋值就不能改变
2、char数组
(1)打印char数组的名字即打印数组的内容
(2)对于字符数组,'\0'是结束标志
字符 '\0' = 数组0 可以认为字符'\0'的ASCII码就是0
要保存5个字符,就要把字符数组长度声明为6
(3)strcpy()和memset()
给一个字符串数组赋值 strcpy(),自动为字符串补 '\0'
在使用strcpy之前,要调用memset(str,0,sizeof(str));初始化一片内存
str -> 初始化内存的起始位置
0 -> 初始化的值 (初始化为0,清除垃圾数字)
sizeof(str) -> 初始化空间的大小
(4)strlen(str)
计算实际存储的字符个数,不包括'\0'
(5)strcmp(str1,str2)
比较2个字符串是否相等
实际比较的两个字符串的ASCII码的大小,返回0则内容一样
(6)字符串的拆分 strtok(a,b);
a、被拆分的字符串名字,当此处是NULL时,表示要拆分的不是新串
b、分隔符号
此函数调用一次,拆分一次
当此函数返回NULL时,则表明拆分完毕
"Hello World"叫字符串常量,存储在data数据区里,类型就是char*
其值是一个地址,指向数据区里的一块空间
数据区里的空间保存的内容就是"Hello World"
char line[50];
strcpy(line,"1:liucy:20:male");
char *p = strtok(line , ":"); //p指针指向的变量
cout << p << endl;
while(p != NULL){
p = strtok ( NULL , ":" );
cout << p << endl;
}
(7) strcpy(name,"1234"); //name是个变量,可以改变
strcpy(p,"1234"); //p是个指针,指向一个字符串,是常量,不能改变
p = "1234"; //这样是正确的
指针指向的是常量,不能通过指针改其值,若指向变量,则可以通过指针改变变量的值
(8)字符串连接 strcat(sub,"world");
连接条件:sub字符串数组的剩余长度要大于连接的字符串长度
char *p = "Hello";
strcat(p,""World); //error 指针指向一个常量,不能改变,所以指针后面不能添加东西
3、通过指针传递参数
通过传递地址。改变变量的值
#include <iostream>
using namespace std;
void fn (int *pa){
*pa = 2 * (*pa); //*pa是指针pa指向的变量的值,在此做的操作,会对变量造成永久的改变
}
int main(){
int a = 100;
fn(&a); //把a的地址传个fn函数
cout << a << endl;
return 0;
}
4、课堂练习
字符串 “1:huxinzhe:20:male”
要求:声明一个结构
Person{
int id;
char name[50];
int age;
char gender[10];
}
拆分赋值 提示:atoi可以把字符串转换成int型
#include <iostream>
#include <string.h>
#include <stdlib.h>
using namespace std;
struct Person{
int id;
char name[20];
int age;
char gender[10];
};
int main(){
Person per;
char line[50];
while(true){
cout << "enter a string>";
cin>>line;
if(strcmp(line,"exit")==0)
break;
char *p = strtok(line , ":");
per.id = atoi(p);
p = strtok(NULL , ":");
strcpy(per.name,p);
p = strtok(NULL , ":");
per.age = atoi(p);
p = strtok(NULL , ":");
strcpy(per.gender,p);
cout <<"per.id = " <<per.id << endl;
cout <<"per.name = " <<per.name << endl;
cout <<"per.age = " <<per.age << endl;
cout <<"per.gender = " <<per.gender << endl;
}
return 0;
}
5、内存管理
堆(heap):动态的内存申请与释放
堆空间不能通过像( 数据区,栈)变量访问空间,要使用指针保存地址
(1)堆空间的管理 new / delete
new int
new Person
new 操作符返回值是地址,用指针保存
int *p = new int; //在堆空间中申请4个字节
*p = 100; //赋值
delete p; //释放空间
(2)动态申请空间,对内存的使用变的方便,能更好的控制对内存的占用
动态内存的指针不能重复赋值,这样会造成内存丢失
释放空间后,还可以通过指针访问原来的数据。
释放空间后,还可以再次通过指针赋值,释放的意思就是表示曾经申请的内存的标记位更改,别人可以使用
这样会造成过期的指针更改重要数据,建议在释放指针以后 ,把指针赋值为NULL ,确保数据安全
delete p ;
p = NULL :
(3)在申请空间的同时初始化
int *p = new int(123); 申请4个字节大小,保存数字“123”
(4)int *p = new int[10]; 一次在堆里申请10个int,并且申请的内存是连续的。
*(p+1) , p[1] 都能取到下一个数据
此时的*p就是第一个元素的值
在释放的时候 delete[] p; p = NULL; 确保释放的是多个指针
(5)在堆空间里申请结构
Person *p = new Person;
p->id = 1;
// (*p).id = 1;
6、 传递参数
最好传地址,节省内存(因为不用进行入栈,出栈操作,直接用指针访问数据)
在参数前加const,可避免在函数内部对数据进行非法修改
当一个数组做参数时,编译器会自动把数组的首地址传个函数
所以在函数内部对数组的更改,就是通过指针对数组中元素的永久性更改
7、数组,存数据个数不限,通过一个函数insert()存数据,disp()打印数组
C++ 笔记 2007年3月30日
1、数组是自动分配空间,指针要手工分配空间(int *p = new int;)
2、在Unix上,程序出现段错误的时候,系统会生成core 文件,会把出现错误的那一刻的程序镜像保存在此文件中
3、结构的成员变量出现数组:
struct Account{
long id ;
//char name[ 50 ] ;
//char password[ 10 ] ;
char * pname ;
char * ppwd ;
double balance ;
};
数组长度100-200字节,最好用数组做,这样方便
数组很大的时候,可以考虑用指针,赋值的时候要注意:
a.pname = new char[50];
strcpy(a.pname , "huxinzhe");
在结构声明时,不能给成员变量赋值
结构声明只是声明了一种数据类型,还没有为这个结构申请空间,赋值的数据没有空间保存
4、危险指针的用法
使用NULL指针的内容 ---隐蔽性不高,容易发现
使用没有初始化的指针 --- 会导致无意当中修改其它变量,破坏性很大,避免的办法 int *p = NULL;
使用已经被delete的指针内容 --- 修改其它变量 避免的办法 delete p ; p=NULL;
返回一个局部变量的地址
由函数申请空间,由调用者释放
#include <iostream>
using namespace std;
char * readLine(){
char * line = new char[ 100 ] ; //在函数中申请了空间,但是没有地方释放
strcpy( line , "hello world" ) ;
cout<<"line = " << line << endl;
return line ; //返回变量的指针,若是局部变量,在函数返回的时候,就消失了,若在堆中申请空间,没有释放空间的机会
}
int main(){
char * p = readLine();
cout<<"return from readLine "<<endl;
cout<<"p = " << p << endl;
delete p; //释放别人申请的空间,不安全
return 0 ;
}
解决的办法 : 在函数外部申请一个指针并初始化,作为参数传给函数,这样可以保存数据并返回
在参数中传数组的起始地址,和数组长度
只要是参数传指针,指针在传给函数之前要进行初始化
5、编译的时候不想看到警告信息 g++ -w
6、函数指针的声明:
函数的名字就是这个函数的首地址
函数指针之间的赋值,也要同类型的
函数指针是不需要释放的。
函数指针,设计通用算法
7、多层指针
int i = 10;
int * p = &i;
int ** pp =&p;
int**(*ppp) = & pp;
*p = i ;
*pp = p;
**pp = i;
多层指针的使用 :
int ia[2][3] = {{11,12,13},
{21,22,23}}; //声明并定义一个二维数组
cout << ia[0] << endl; //打印的是ia的第一个数组的首地址
int * p = ia[0]; //保存ia第一个数组的首地址
cout << a[0][2] <<endl;
cout << p[2] <<endl; //与上面一行具有相同的效果 。
ia[0]的类型就是int*类型的,指向ia的第一个数组
ia也是个指针类型,指向那个二维数组 ,是 int**
二维数组是一个二级指针
二维数组的每个元素是一级指针
int (*pp)[3] = ia; //行指针,指向二维数组的一行,[]中的数字表示每一行的元素个数
cout << pp[1][1] <<endl; //通过二级指针去数组元素 pp[1][1] <=> *( *(pp+1) +1 )
行指针可以描述指向数组的元素个数,以行为单位,加1就是指针指到下一行
打印数组在内存中地址
int (*p1)[3] --> p1的类型是int[3] p1+1,走3个int,一个数组的长度
int ** p2 --> p2的类型是int* p2+1,走1个int
8、char* names[100]; 声明一个数组,每个元素类型是char*
chat* names[3] = {"liucy","huxz","tangliang"}; //这样的声明更清晰的说明是char的二维数组
for(int i = 0 ; i < 3 ; i++){
cout << names[i] <<endl; //打印每个元素
}
9、void*
任何类型的变量的地址都可以把地址存进来
通用指针的存储,不能做任何运算,是纯粹的存储地址而用
10、const与指针
const int *p 常量指针 不能通过指针改变变量的值 const离int近,值不能改
int* const p 指针常量 指针不能改,指向的对象是固定的,但可以通过指针改变变量的值
11、指针的要求
指针的声明和基本运算
数组与指针的关系,结构和指针的关系,字符指针的用法
堆空间及危险用法
12、引用
引用就是一个变量的别名 int &iR= i; //给i起的别名iR
引用声明的时候必须初始化(与一个变量保持关联关系),一旦赋值,就不能把别名再给别的变量了
int iL = 100;
iR = iL; // 相当于把i的值改变了,iR还是i的别名
13、引用的使用
以引用的形式传参数 fn(int &a)
在函数内部的操作就是对此实参进行操作
一般情况下 (1)在真的想改变实参的值的时候
(2)在实参的大小比较大的时候,传引用,这样系统不生成临时变量,减小耗费内存
的时候传引用。
fn(const int &a) 这样传引用,不会创建临时变量,也不会改变实参的值
14、周末项目
(1)完善项目 把密码用char数组保存
(2)写一个函数,对所有类型的数组进行排序
提示:通过函数指针实现对struct传参数
void* 指针,存储所有类型指针
void sort (int perlen , int *p , int len , void (*order)(int * ,int *)){
char *p1 = (char*)p; //char是基本类型里最小的,可以模拟步长
for(int i = 0 ; i < len ; i ++{
for(int j = 0 ; i < len ; i++){
order(p+i*prolen,p+j*prolen);
}
}
)
C++笔记 2007年4月2日
1、排序函数
void order(int* p1, int* p2){
if(*p1 > *p2){
int temp = *p1;
*p1 = *p2;
*p2 = temp;
}
}
void sort (int *p , int len , int perLen , void (*pOrder)(void* , void*)){
char* pt = (char*)p;
for(int i = 0 ; i < len ; i++){
for(int j = i ; j < len ; i ++){
pOrder(pt+i*perLen , pt+j*perLen);
}
}
}
(1)将输入参数int* -> void* (void* 可以存储任何类型的地址,可以通用 )
(2)表示数组的第i个元素,事先要知道数组每个元素的大小(参数传入)
i*perLen => 当不知道指针类型时,表示走一步,应越过的字节数
p+i*perLen => 表示第i个元素的地址
(3)具体的数组要有具体的排序方法
调用函数,当不知道函数名时,可以通过函数指针调用
==================================================
sort.h
==================================================
void sort( void * p , int len , int perLen ,
void (*pOrder)(void * , void * ) );
==================================================
sort.cc
==================================================
void sort( void * p , int len , int perLen ,
void (*pOrder)(void * , void * ) ){
char* pt = (char*)p ;
for( int i = 0 ; i < len ; i++ ){
for( int j = i ; j < len ; j++ ){
pOrder(pt+i*perLen,pt+j*perLen ) ;
}
}
}
==================================================
main.cc
==================================================
#include <iostream>
#include "sort.h"
using namespace std;
void orderInt( void * p1 , void * p2 ){
int * t1 = (int*)p1 ;
int * t2 = (int*)p2 ;
if( *t1 > *t2 ){
int temp = *t1 ;
*t1 = *t2 ;
*t2 = temp ;
}
}
struct Person{
char name[ 40 ] ;
int age ;
int id ;
} ;
void orderByAge( void * p1 , void* p2 ){
Person * t1 = (Person*)p1 ;
Person * t2 = (Person*)p2 ;
if( t1->age > t2->age ){
Person t = *t1 ;
*t1 = *t2 ;
*t2 = t ;
}
}
void orderById( void *p1 , void* p2 ){
Person* t1 = (Person*)p1 ;
Person* t2 = (Person*)p2 ;
if( t1->id > t2->id ){
Person t = *t1 ;
*t1 = *t2 ;
*t2 = t ;
}
}
void orderByName( void * p1 , void* p2 ){
Person* t1 = (Person*)p1 ;
Person* t2 = (Person*)p2 ;
if( strcmp( t1->name , t2->name ) > 0 ){
Person t = *t1 ;
*t1 = *t2 ;
*t2 = t ;
}
}
int main(){
int ia[] = { 3,1,6,3,6,8,3,468,89 };
sort( ia , 9, sizeof(int), orderInt );
for( int i = 0 ; i < 9 ; i++ ){
cout<<ia[i] <<" " ;
}
cout<<endl;
Person pers[ 3 ] ;
pers[0].id = 1 ;
pers[0].age = 29 ;
strcpy( pers[0].name , "liucy" ) ;
pers[1].id = 2 ;
pers[1].age = 28 ;
strcpy( pers[1].name , "huxinzhe" ) ;
pers[2].id = 3 ;
pers[2].age = 26 ;
strcpy( pers[2].name , "xuehailu" ) ;
sort( pers , 3 , sizeof(Person), orderByAge );
for( int i = 0 ; i < 3 ; i++ ){
cout<<pers[i].name <<","<<pers[i].age<<",";
cout<<pers[i].id<<endl ;
}
sort( pers, 3, sizeof( Person) , orderById ) ;
for( int i = 0 ; i < 3 ; i++ ){
cout<<pers[i].name <<","<<pers[i].age<<",";
cout<<pers[i].id<<endl ;
}
sort( pers , 3 , sizeof( Person ) , orderByName );
for( int i = 0 ; i < 3 ; i++ ){
cout<<pers[i].name <<","<<pers[i].age<<",";
cout<<pers[i].id <<endl;
}
return 0 ;
}
2、面向对象
封装:对象表示
继承:更好的代码重用
多态
对象的组成 : 属性 成员变量
行为 函数
面向过程的表示方法:
数据与函数分离,关系松散
封装的作用,把数据和函数封装到一起,保证数据专用
全局函数:在类外面的函数,要使用成员变量,要通过参数传进来
成员函数:在类内,可直接使用自己类的成员变量
对于类的变量的初始化:
Person p ;
strcpy(p.name , "liucy");
p.age = 23;
p.speak();
对成员变量和成员函数的使用都要通过类的对象
public 关键字,表示在其他地方可以使用
默认是私有的,在main函数中不能使用
成员变量和成员函数依赖于类的对象(实例)
类型是对对象的描述
对象是类型的实例
对象自己的成员函数访问自己的成员变量
什么是类?类由什么组成?
怎么使用类?及类和对象的关系?
成员变量和成员函数归谁所有?
面向对象的方法写程序
(1)首先写一个类,描述对象
用变量表示属性,函数表示行为
(2)调用函数
创建一个类的对象,通过对象调用函数
C++笔记 2007年4月3日
1、类型封装
类 --> 对象
描述 : (1)属性
(2)行为 ---属性和行为是属于类的
创建对象。
2、构造函数 --- 初始化对象
(1)构造函数名字必须与类名一样
(2)构造函数不能写返回类型
构造函数在创建对象时,系统自动调用
构造函数允许重载,按用户要求,适应多种情况
当类中一个构造函数都没有的时候,系统提供默认无参的构造函数
但如果在类中定义了一个构造函数,系统就不提供默认的了,所以,建议,在写构造函数时,都要写一个无参的构造函数
3、对类的安全的定义
变量 -> 私有 -> private 保护变量,防止外界随意修改,只能在类的内部使用(只能被自己的成员函数使用)
函数 -> 公有 -> public
为保证私有变量也能为外界访问,在类内部提供set,get方法
set方法没有返回值,但要求有参数
get方法肯定有返回值,没有参数
4、定义类的步骤
(1)写属性的行为
成员变量 函数
(2)成员变量--> private
成员函数 --> public
(3)特殊函数
构造函数,建议写2个。一个有参数的,一个无参的
get,set函数,一个成员变量对应一对get,set函数,是外界访问此变量的唯一途径
对于setXXX函数,没有返回值,有参数,参数的类型与被赋值的属性类型一致。
对于getXXX函数,没有参数,有返回值,返回值的类型与输出的属性类型一致。
类的行为
5、封装
定义类的过程就是封装
练习:封装一个account类型
属性:id , password ,name ,balance
行为:save , withdraw , query
6、把account类拆成多文件结构
(1)方便快速浏览整个类定义
(2)使用方便,包含头文件即可
long Account::getId(){...........}
在函数实现时,函数前把类名加上,确定函数的所有权,避免2个类中有同名的函数,编译出错。
" :: "称为域访问控制符。
7、Person per ; 创建了一个对象,系统调用构造函数。
Person *p = NULL ; 声明一个类的指针,系统是不会调用构造函数的
Person * p = NULL;
p = new Person ; 在堆中申请一个类空间
delete p;
在堆中的数据没有名字,只能通过指针访问类对象,访问类的成员变量:
(*p).sayHello();
p->sayHello();
Person ps[5];声明一个Person类型的数组,会调用5次构造函数
数组在声明的时候,系统会为其分配空间
并且在声明数组的时候,没有机会指定构造函数,只会调用无参的构造函数
当一个类定义中没有无参的构造函数,但要声明一个类的数组时,可以声明一个指针数组
Person *ps[5]; ---声明指针的时候,类对象没有创建,数组中每个元素都是Person类型的指针,达到:
(1)不用构造函数
(2)实现使用对象
for(int i = 0 ; i <5 ; i++){
ps[i] = new Person(i , 20+i); //用循环初始化每个指针
}
for(int i = 0 ; i < 5 ; i++){
delete ps[i]; //释放指针指向的空间
ps[i] = NULL ;
}
8、课堂练习:
要求:main()函数中不能写代码,在运行程序时,打印“Hello World”
答案:全局变量的初始化操作在main函数执行之前就已经初始化了。因此,可以写一个全局变量的类,其无参构造函数写输出语句。
然后声明一个类的全局对象。
9、在构造函数中,当成员变量名称和形参名称相同,避免冲突的方法:
在每个对象内部都有一个名为this的指针,指向自己,使用自己的成员变量 this->name
10、析构函数
当对象的生命周期结束,系统要回收空间,会自动被调用
申请的所有资源(new char[20]),在析构函数中释放
若对象是在main函数中的变量,在main函数结束之后,才会调用析构函数
若对象是一个函数中的局部变量,在函数返回时,调用析构函数
析构函数调用 : 对象生命周期结束,系统自动调用析构函数
可以把释放资源的代码写在析构函数中
析构函数的写法:构造函数前加 “~”
不能重载,不能有参数
若不写析构函数,系统会提供默认的析构函数
当类中用到了系统空间:new内存,打开文件
就需要写析构函数,释放资源
11、作业:(1)利用面向对象的思想实现栈结构,并自己写main函数测试。
(2)把原来的银行系统改成面向对象的形式。
C++笔记 2007年04月05日
1、 本对象 子类对象 其他函数
private属性 可见 不可见 不可见
protected属性 可见 可见 不可见
public属性 可见 可见 可见
public extends protected extends private extends
父类的private属性 不能访问 不能访问 不能访问
父类的protected属性 变成protected 不变 变成private,子类可以访问,子类的子类不能访问
父类的public属性 不变 变成protected 变成private,子类可以访问,子类的子类不能访问
2、 构造函数有很多种,因为没有指定构造函数,就会默认使用无参的构造函数。如果父类没有无参的构造函数,那么就会出现编译错误。
可以使用这种形式Teacher(char* name, int age, double salary):Person(name,age){......},指定使用父类的有参构造函数。
3、多态的特征:
父类的指针可以指向子类的对象
通过父类指针只能调用父类中声明的方法
通过指针调用函数的时候,若函数是个虚函数,表现出来的行为是对象自身的行为
4、产生多态:(1)指针
(2)引用
父类的引用可以引用一个子类对象
通过父类引用只能调用父类函数
调用一个父类被覆盖了的,虚函数,能调用子类的函数
5、一个子类继承一个父类 --- 单继承
一个子类继承多个父类 --- 多继承
class SpiderMan : public Spider , public Person{....}
6、菱形继承,解决重复元素的冲突
让两个父类同时虚继承一个超类,把多继承中的重复元素放在超父类中
当有多个子类同时虚继承一个父类的时候,只有一个子类真正的构造父类
class Spider : vertual public Animal{.....};
class Person : vertual public Animal{.....};
class SpiderMan :public Person , public Spider{....};
多继承尽量不要使用三层以上
7、抽象类
只有函数声明,没有函数实现
纯虚函数:没有实现的函数 virtual void writeLog(char*)=0;
若不写" =0 ",则系统会认为是函数声明,会试图去别的" .cc "文件中去找函数实现
含有纯虚函数的类称为抽象类,是抽象数据类型,不能创建对象
抽象类型就是为了被别人继承的,子类覆盖纯虚函数,提供函数实现
通过父类规范子类的用法
如果子类没有完全实现抽象父类的所有纯虚函数,则认为子类还是一个抽象数据类型
用到virtual的地方:
(1)继承
(2)多继承
(3)纯虚函数
抽象类的规范很重要,在工程中,对于项目的并行开发很重要
而且对于项目中的改动,能方便的应付
用指针指向相应的子类对象,方便的调用子类的函数
8、友员
针对类来说,自身的私有变量,不能被别的类访问,但是,如果授权给一个类为自己的友员,就可以访问他的私有属性
可以作为友员的东西:另一个类,一个全局函数。
实现友员的授权:
class Girl;
class Person{
........
friend class Girl; //友员类的声明-->授权给Girl类,成为自己的友员,可以访问自己的私有变量了
}
----------------------------------------------------------------------------------------------------------------
class Girl;
class Person{
........
friend void fn(); //友员函数的声明-->授权给fn函数,成为自己的友员,可以访问自己的私有变量了
}
友员不是类的一部分
若不是互为友员,则不能访问友员类的私有变量
友员的使用:
Bus把售票员作为自己的友员,访问自己的私有变量,即装载乘客的数组
友员在项目中的使用
9、静态数据
在类中定义一个静态数据 (实际上就是一种全局变量)
(1)不依赖于对象,在对象不存在之前就已经存在了
(2)所有对象共享
与全局变量的区别:
(1)使用的类中的静态变量,必须通过类名使用
(2)而且受访问控制符号的限制
(3)静态变量在类中声明的时候不能赋值,要在类外初始化
class A{
public :
static int a;
};
int A::a = 100; //此时才分配空间
int main(){
cout << A::a <<endl; //静态变量的用法,不依赖对象,直接使用
}
与成员变量的区别
(1)成员变量依赖与对象,类对象不存在,成员变量也不存在
静态变量不依赖于对象,只要有类声明,就存在
(2)所有对象共享一份静态变量
10、静态函数
在函数前加static
不依赖于对象,没有对象依然可以通过类名调用静态函数
A::fn();
在类声明的时候,静态函数和静态变量就存在了
静态函数只能使用静态变量,不能使用成员变量
11、拷贝构造函数
#include <iostream>
using namespace std;
class Person{
public:
Person(){ cout<<"Person()"<<endl; }
~Person(){ cout<<"~Person() "<<endl;}
void speak(){ cout<<"hello"<<endl; }
};
void fn( Person p ){ //这里的传递时值传递,形参是值传递,这里的形参的创建是使用拷贝构造函数
p.speak();
}
int main(){
Person p ;
fn( p ) ;
return 0 ;
}
输出结果:
Person()
hello
~Person()
~Person() //2次析构,因为其中调用了的系统提供的拷贝构造函数,构造出一个新对象
拷贝构造函数,以一个已存在的对象为模版创建一个新对象
声明方法: Person(const Person & p){...} //即节省空间,又保证模版不会被修改
默认拷贝构造函数,执行的就是简单的内存拷贝 --- 浅拷贝
(1)浅拷贝
只拷贝地址,而不是对指针指向空间的拷贝,会造成2个指针指向同一个空间
(2)深拷贝
为指针创建新空间,拷贝指针指向空间的内容
调用拷贝构造函数的时机:
(1)在值传递的时候
(2)A a; //默认构造函数
A a1 = a; //在声明的时候,用一个对象赋值,使用的是拷贝构造函数
(3)A a1;
A a2(a1); //显示的调用拷贝构造函数,传的参数是个对象
什么时候需要自己写拷贝构造函数?
使用了动态内存,就会面临浅拷贝的现象,需要自己写拷贝构造函数
12、运算符重载
a1 = a2;
系统调用了函数 operator=
相当于执行了这样的操作: a1.operator=(a2);
Student& operator= (const Student &a);
函数返回左值,返回值为引用
函数返回右值,返回值为本身
/*
*student a1;
*student a2;
*a1=a2; <=> a1.operator=(a2);
*/
Student& operator= (const Student &a){
age = a.age;
id = a.id;
strcpy(name , a.name); //不用new出一块空间,因为在声明a1和a2的时候,两个指针都指向一块自己的空间,
把指针指向的变量拷贝过去,即完成赋值
return *this;
}
当在堆中申请空间,则覆盖赋值运算符(" = ")
作业:银行系统的账户类
把name , password 改用指针保存
(1)析构函数
(2)拷贝构造
(3)重载赋值运算符
C++笔记 2007年4月6日
1、拷贝构造函数和运算符重载
(1)当类的成员变量中出现指针类型的时候,需要动态申请空间,这样就需要解决浅拷贝的问题
在声明对象的同时用另一个对象为其赋值,会调用拷贝构造函数。
系统提供的默认拷贝构造函数,是浅拷贝,我们可以自己写一个拷贝构造函数,把指针指向的变量也拷贝过去
(2)类中的成员变量出现指针类型,当两个对象都创建出来了以后,相互赋值的时候,就需要重载赋值运算符号
手工为指针指向的变量赋值
2、其他的运算符号重载
对于对象之间的加减操作,系统是不允许的,但通过自己的运算符重载,按照自己的规则,实现对象之间的运算操作。
Integer operator+(const Integer& i){
int t = *p + *(i.p);
Integer temp(t);
return temp;
}
(1)自增运算符
前++是左值,返回引用
后++是右值,返回临时值
"++"运算符的优先级比"+"高
Integer& operator++(){}
Integer operator++(int i){}
int i是用于区别前++和后++的,是没有实际意义的参数,称为哑元,必须是int类型
前++和后++的操作,主要区别就是返回的值不同,内部都是把变量加1。
前++,(++i)先加后用,返回加1之后的变量值,可以把变量直接加1,就返回,所有可以直接返回引用
后++,(i++)先用后加,返回加1之前的变量值,就是要返回原来的旧值,
这样需要在重载运算符的函数内部创建一个对象保存旧值,再进行加1运算,返回这个旧值本身。
(2)重载"="
实现用一个int类型给一个Integer对象赋值
Integer& operator=(int i){ //赋值运算,把对象内的int值改变,返回本身即可,所以返回值是引用
*p = i; //手工将int类型的值赋到对象内
return *this;
}
(3)运算符重载
不仅可以用类的成员函数实现,也可以用普通函数实现
用成员函数实现,参数只有一个,运算符左边的是自身,右边的是参数 a1.operator=(a2);
用普通函数实现,参数需要两个,第一个参数是运算符左边的值,第二个参数是运算符右边的值,作为友员函数重载
operator(a1,a2);
(4)推荐原则
所有一元运算符 --- 成员重载 "=","[]"只能成员重载
二元运算符 --- 友员重载
Integer对象类型与int类型相加的时候,
实现 5+i 而且也能 i+5
可以用友员重载2次
friend Integer operator+(const Integer& i , int a);
friend Integer operator+(int a , const Integer& i ); //在类中友员函数的声明
(5)强制类型转换运算符
operator int(){......} //强转成int类型的声明
3、流操作符的重载
(1)输出流操作符只能使用友员方式重载
friend ostream& operator<< (ostream & o,Integer & i); //声明友员函数
ostream& operator<< (ostream & o ,Integer & i){ //实现
o << *(i.p) ; //输出对象内的*p,即指针指向的变量
return o;
}
cout << i ; <=> operator(cout,i);
(2)输入运算符也能重载,实现对象的读入,只能使用友员函数重载
friend istream& operator>> (istream & in,Integer & i); //声明友员函数
istream& operator>> (istream & in ,Integer & i){ //实现
in >> *(i.p) ; //把读入的数据放到*p指向的变量中
return in;
}
(3)为什么只能用友员重载?
因为cin cout 的位置是固定的, cin >> i ; cout << i ;
这样不能利用对象本身调用重载的流操作符 i.operator(cout) ->这样是不正确的
只能使用友员重载,把对象和cout都当做参数传进去,这样就能操作了
练习:账号类,直接输出账号
Account a1;
cout << a1 << endl;
friend ostream& operator<< (ostream & o,Account & a);
ostream& operator<< (ostream & o ,Account & a){
o << "Name : " << a.name <<endl;
o << "password : " << a.password << endl;
o << "id : " << a.id << endl;
o << "balance : " << a.balance ;
return o;
}
friend istream& operator>> (istream & in,Account & a);
istream& operator>> (istream & in ,Account & a){
cout << "enter your name >";
in >> a.name;
cout << "enter your password >";
in >> a.password;
cout << "enter your id >";
in >> a.id;
cout << "enter your balance >";
in >> a.balance;
return in;
}
作业 :
写一个类叫“rmb”
RMB{
int * yuan;
int * jiao;
int * fen;
RMB();
RMB(int y , int j ,int f);
RMB(const RMB & r);
~RMB();
operator=
friend ostream& operator<< (ostream & o,RMB &r);
friend istream& operator>> (istream & in,RMB &r);
人民币之间的加减运算,及与int的乘法
operator double(){} //强转运算
operator float(){}
}
C++笔记 第十三天 2007年4月9日
1、数据 内存中 变量
磁盘上 文件
2、把数据从其他的设备搬到内存中 --- 输入 --- 读
把内存中的数据放到其他设备中 --- 输出 --- 写
3、流
物质的定向移动,输入输出流中是数据的定向移动
输入流的源头 : 文件 目的地:内存
输出流的源头 : 内存 目的地:文件
4、标准输出设备 --- 显示器
标准输入设备 --- 键盘
键盘 --- 内存 --- 显示器
| |
输入操作 输出操作
输入输出流 : 内存与磁盘之间,内存与标准输入输出设备之间的
5、cout 源 :变量 目的地 :显示器
cin 键盘 内存中某一变量
6、标准输入流 cin istream的一个对象
标准输出流 cout ostream的一个对象
标准错误流 cerr 目的地都是屏幕,用cout替代
7、cin
是一个带有缓冲的标准的输入对象,默认输入设备是键盘
(1) >> : 自动校验数据类型
遇到回车才会开始读数据,遇到空格就结束,只能读一个单词
流是数据的定向移动,被读走的数据就消失,没有读走的数据会一直留在流中,直到流的消失,数据也跟着消失
流中有数据,就会阻塞,等待读取 --- 所有输入方法的特性
为什么 ">>"可以连续使用? 因为返回就是istream对象本身的引用
注意 : ">>" 不读回车,不读空格
(2)get(): 每次 读一个字符。返回一个整数,实际上是读到字符的ASCII码
把回车,空格都当作普通字符读出来
(3)get(char&):把读到的内容存到参数中
cin.get(arr[0]).get(arr[1]); //get(char&)返回cin本身,可以连续使用
(4)getline(str,256) : 读取一行,包括空格
对于回车,只读走,不保存
会读取数组长度减1个字符,最后一个放'\0'
输入数据超过给定的空间 (1)截断数据,剩下的数据还在流里
(2)设置一个错误标记,调用cin.clear(),清除错误,继续工作
#include <iostream>
using namespace std;
int main(){
int age;
char name[20] ;
cout << "enter your age >";
cin >> age;
cin.get(); //读取流中残余的回车,以便getline能正常工作
// cin.ignore(20,'\n'); //忽略20个字符或者碰到回车,从流中清除
cout << "enter your name >";
cin.getline(name,20);
cout << "your age is :" << age << endl;
cout << "your name is :" << name << endl;
}
(5)read(char*,int) char*是存结果的地址,int是读的长度,并且不能比前面的数组的空间大
读满为止 ,特殊字符也当做普通字符处理
超出的部分仍然存在流里面
只要数据没读满,一直阻塞
不会自动补'\0' --- 传参数的时候,数组长度传减1的长度,补齐'\0'以免乱码
所有输入流的共同特征:只要没数据就阻塞
读不完的就留在流里
(6)cin.ignore(255,'\n') 忽略255个字符或者遇到'\n',如果在前255个字符中出现'\n',则就忽略到'\n'之前的字符
(7)peek() 查看流里的第一个字符是什么
只察看,不读走
(8)putback() 向流中插入字符,前提必须有空位的时候
必须与get()一起使用,用get()取出一个字符,才能用putback()插入一个字符
(9)cin.fail()判断是否出现错误标志,一切正常返回false
当用cin读取的数据类型出现错误的时候,这是一种不可恢复的错误,用cin.clear()是不能清除错误标记的
在键盘上读取,用getline()可以确保输入流的正常形成,读取后采取强制转换类型得到自己需要的数据
8、ifstream
(1)需要#include <fstream>头文件
ifstream ifs("test.txt"); //创建一个ifstream的对象,打开文件,给构造函数传如一个参数,就是文要打开的文件名
//文件的在当前目录下找,也可以用相对路径或绝对路径找文件
在打开文件之后,立即判断打开文件是否成功
if( ifs.fail() ){
cout << "Can't open test " <<endl;
return 0;
}
if( ! ifs ){ //也可以这样判断文件打开是否出错
cout << "Can't open test " <<endl;
return 0;
}
(2)在文件结束的时候都会有"EOF"标志,作为文件结束的标志符
可以用判断是否读到"EOF",来判断时候读到文件尾了
if(ifs.eof()){
break;
}
(3)对于一个进程可打开文件的数量是有数的,所以文件属于资源
所以在使用完毕以后,要关闭文件输入流
练习:(1)把/etc/passwd 文件打印出来
(2)把文件中不以"#"开头的内容读出来
peek(),ignore()
9、输出操作cout
(1) << 操作 被输出到屏幕上的东西,只所以能输出,都是以字符串类型输出
也就是说这个操作有自动类型转换的功能
(2)put() 返回cout引用,可以连续调用
(3)write(buf,len) 按指定长度写到屏幕上 buf是char*类型
(4)cout.width(10); 打印的内容总共占10个字符,并靠右对齐
只对其后边的一个cout生效
(5)cout.fill('#'); 用'#'补齐空位
主要调用一次,对以后的都生效
(6)setf()操作,控制格式,教材188页
(7)特殊字符
'\r' 表示回退一个格,再输出
'\n' 回车
\\ 输出一个'\',因为'\'会被认为是转义字符
'\t' 一个tab键
(8)输出控制符
oct 按八进制输出 “0”
dec 使进制输出
hex 按十六进制输出 “0x”
flush 清空缓冲区 带缓冲区是因为和外部设备交涉,这样能减少向屏幕输出的次数,提高效率
回车、程序结束和flush都是刷新缓存的命令
cout << "a" <<flush ;
10、ofstream
打开文件,把数据写进文件,关闭文件
ofstream ofs("ofstream.txt"); //打开文件,若文件不存在,创建,存在,打开
if(ofs.fail()){ //写文件失败,一般是权限问题
cout << "open file error "<<endl;
return 0;
}
在iostream头文件中cin cout对象已经被声明,可以直接使用,因为标准输入输出设备是唯一的,系统声明
但是fstream的对象要程序员来声明,因为对文件的输入输出是不唯一的
ofstream ofs("ofstream.txt" , ios::app); //以追加的形式向文件中写
ios::trunc 默认方式 把文件内容清空,写新的文件
ios::nocreate 不创建新文件
ios::noreplace 不改写,要创建新文件
组合多个 ofstream ofs("ofstream.txt" , ios::app | ios::in | ios::binary);
11、读写二进制文件
(1)ios::binary
(2)read/write 方法读写二进制文件,因为这两个方法只需要起始位置和大小就可以工作
作业:(1)int readInt(){}
double readDouble(){}
强转函数,要求容错能力强,要求有可靠的手段通知用户是否出错
(2)根据用户输入的用户名,打印他的用户id,home,shell,如果用户不存在,通知用户
strtok(),strcmp()
C++笔记 第十四天 2007年4月10日
1、对文件的分类
(1)文本文件:每个字节都是有效的可显示的ASCII码 ,getline() , >>
(2)二进制文件:字节是连续的,不能用vi , more查看文件内容,read按字节数读取 , write
100 是整数的100,占4个字节
“100”是字符数组,占3个字节
2、异常
(1)人为错误:由于程序员编码不当
客观错误:不能避免的错误
(2)通过返回值判断程序的错误情况,对调用者的要求高,要写if()else()判断,而且对于返回值的含义要非常的清楚
所以C++中提供了异常处理机制
3 异常的工作原理:
1) 程序员写的代码在出现意外的地方自动产生一个异常,然后抛出一个异常对象。
2) 对象被传递到负责异常处理的地方。
throw 1; //抛出异常,让调用者处理
3) 由负责异常处理的代码进行统一的异常处理。 try{}catch(){}
4) 异常对象包含有意外发生的详细信息。
4 异常代码的格式:
抛出: throw 异常名字;
处理: ppt368
try {
} catch( 异常1 int) {
处理代码;
} catch( 异常2 const char* ) {
处理代码;
}
程序正常的时候,catch块什么也不做,当出现异常的时候,程序从产生异常的地方跳到catch块中
异常处理完毕之后,不会回到发生异常的地方。
用try{}catch(){}处理异常是一种强制手段,出现异常,进程结束
catch()中的异常类型如果与抛出的异常类型不一致,则捕获不到
5 每个catch只能处理一种类型的异常,catch块会依次执行。
6 catch(...){
处理代码;
}
可以捕获任意类型的异常,但是它不能判断是什么类型的异常,一般把它放在最后一个catch块。
但这种捕获没有针对性
7 异常的传播特性:
层级传播:异常会逐层抛出
产生异常之后,程序立即跳转到最近的一层捕获异常的语句,如果当前没有捕获语句,或者没有匹配的catch块,那么程序会跳出当前的函数回到调用的地方。
如果向上跳到了main函数,还是没有处理异常,程序就会终止进程。
8、封装异常(1)使用字符串描述异常
(2)制定异常号 error No.
(3)disp();
当捕获的异常存在继承关系的时候,要先捕获子类异常,再捕获父类异常
9、内联类
把一个类写到另一个类的内部
异常一般会做成内联类 A::exp ,靠前缀制定自己的命名空间
内联类,private修饰只能在本来中使用,对外是隐藏的,只有public 修饰的才能在外面使用
10、异常总结
(1)检查错误的一种手段
(2)可以用简单数据类型定义,还可以自定义类型
(3)产生 throw e; 异常对象
处理 try{}catch(异常类型){} --- 异常类型要与throw抛出的一样
(4)传播
11、银行项目 --- 面向对象
需求分析的时候,要画用力图
Biz Object 完成业务逻辑
Value Object 操作数据(Account)
Menu 收集用户信息,用户选择的业务
Biz 针对某项业务收集信息
DA 提供文件访问服务
Account 保存数据
C++ 笔记 第十五天 2007年4月11日
1、在头文件中
#ifndef _ACCOUNT_ //预编译选项,表示如果没有定义这个宏
#define _ACCOUNT_ //创建以_ACCOUNT_命名的宏
并声明类
#endif
2、链表
(1)解决数组必须连续存储的问题
链表是可以不连续的,通过每个节点的指针连接
(2)节点中一部分空间用于存放数据,另一部分是一个指向下一个节点的指针
(3)每个节点都是一个结构
struct node{
int data; //存储数据
node* next; //指向下一个节点的指针,是自己这个结构的类型
}
(4)尾节点 --- 链表中的最后一个节点 --- 指针指向NULL
头节点 --- 要访问链表中的元素,必须要知道头节点的位置
把地址放在一个指针中 --- 头指针指向头节点,只是一个指针 --- 是必须存在的元素
(5)对链表的常见操作 --- 增删改查
(6)链表与数组的区别
数组:空间必须连续,数组是定长的,插入和删除需要遍历整个数组,效率不高。
取元素可直接使用下标,访问方便
链表:空间在内存中不必连续,通过指针连接
链表是不定长的,可以随时添加新节点,通过指针关联
对链表的插入删除,不需要移动节点位置,只对指针操作即可
访问元素,要从头指针开始遍历
当数据需要频繁的插入删除的时候,需要使用链表
当改动不大,查询频繁的时候,使用数组
潜规则 : 能用数组就不用链表
======================================================================
link.h
======================================================================
#ifndef _LINK_
#define _LINK_
using namespace std;
class Node{ //节点类
public :
int val; //保存数据
Node* next ; //保存下一个节点的地址
Node(){ //构造函数,把指针初始化为NULL
next = NULL;
}
};
class Link{
protected :
Node* head; //头指针
public :
Link();
~Link();
void insertTail(int);
void insertHead(int);
void del(int);
int indexOf(int); //查询一个元素的下标
void update(int , int);
void disp();
};
#endif
======================================================================
link.cc
======================================================================
#include "link.h"
#include <iostream>
using namespace std;
Link::Link(){
head = NULL;
}
Link:: ~Link(){//释放空间,从头向尾释放
if(head != NULL){
Node *p = head;
head = head->next; //把头节点向后移动
delete p; //抛弃原来的那个头节点
cout << "delete one ... " << endl;
}
}
//尾插入
void Link::insertTail(int v){
Node *p = new Node;
p->val = v;
if(head == NULL){
head = p; //让新节点的指针指向新节点,即把新节点的地址保存在头指针中
return ;
}
Node * temp = head ; //用一个临时指针,从头节点开始找到 尾
while(temp -> next != NULL){ //表示temp不是尾节点
temp = temp -> next ; //用temp后面的一个指针为自己赋值,即指向下一个节点
}
temp -> next = p; //尾插入,最后一个节点的指针保存新节点的地址
}
//头插入
void Link::insertHead(int v){
Node *p = new Node; //创建新节点
p->val = v ; //保存数据
p->next = head; //让新节点的指针和头指针一样指向第一个节点
head = p; //让头节点指向新节点
}
void Link::del(int v){ //找到被删除的节点 ,
if(head == NULL ){
return ;
}
if(head -> val == v){
Node *p = head;
head = head->next;
delete head;
}
Node *p1 = head->next; //找值相同的一个
Node *p2 = head ; //跟在p1后面
while(p1 != NULL){
if(p1->val == v){
p2->next = p1 -> next;
delete p1;
break;
}
p1 = p1->next;
p2 = p2->next;
}
}
int Link::indexOf(int v){ //查询一个元素的下标
Node * p = head ;
int counter = 0 ;
while( p != NULL ){
if( p->val == v ){
return counter ;
}
p=p->next ;
counter++ ;
}
return -1 ;
}
void Link::update(int v1 , int v2){
Node * p = head ;
while( p != NULL ){
if( p->val == v1 ){
p->val = v2 ;
}
p = p->next ;
}
}
void Link::disp(){
Node *p = head;
while(p != NULL){
cout << p->val << " " ;
p = p->next;
}
cout << endl;
}
3、二叉树
每个节点最多只有两个分支的树,它有一个根指针,要指向这棵树的根节点(最顶端的节点).
左子树上的值小于其父节点的值,右子树上的值都大于其父节点上的值。 --- 排序二叉树
(1)周游(遍历) :先序 --- 中左右
中序 --- 左中右
后序 --- 左右中
(2)非常方便查找
二叉查找树的常见操作:
1) 插入. 示例代码如下:
Node* Tree::_insert(int v, Node* r){ //真正实现插入操作,返回插入以后的根
if(r == NULL){ //是一棵空树 (空子树)
Node* p = new Node(v); //创建新节点
r = p; //让新节点成为根或者子节点
return r;
}
if( v < r->val){ //插到左子树上
r->left = _insert(v,r->left);
return r;
}else{ //插到右子树上
r->right = _insert(v,r->right);
return r;
}
}
2) 查找. 示例代码如下:
Node* & find( bnode* & root, const DATA& cd )
{
if( root==NULL ) // 如果root节点是空,则为空树
return root; // 返回root指向的地址,即NULL
else if( root->data==cd ) // 如果root节点就是要查找的数值
return root; // 返回root指向的地址,为了清晰,和上面的分开写
else if( cd < root->data ) // 如果root节点指向的值大于要查找的值
return find( root->left, cd ); // 返回查找root的左子树返回的地址
else
return find( root->right, cd ); // 否则返回查找root的右子树返回的地址
}
3) 删除. 示例代码如下:
被删除的是树根(1)则选择右子树的树根做新树根,左子树可以整个挂在右子树最左侧的一个左节点上
右子树中最左边的一个节点,是最靠近左子树的树根的
(2)让左子树中的最大节点做新树根
Node* _del( int value , Node* r ){
if( r == NULL ){ //删除空树
return r ;
}
if( r->value == value ){ //删除树根
if(r->left==r->right){ //左右子树都是NULL的情况下
delete r ;
return NULL;
}else if( r->right == NULL ){ //只有右子树,没有左子树的时候
Node * p = r;
r = r->left ;
delete p ;
return r ;
}else if( r->left == NULL ){ //只有右子树,没有左子树
Node *p = r ;
r=r->right ;
delete p ;
return r ;
}else{ //左右子树都有
Node * p1 = r -> right ;
Node * p2 = r -> right ;
while( p2->left != NULL ){
p2 = p2->left ;
}
p2->left = r->left ;
delete r ;
return p1 ;
}
}
if( value <= r->value ){
r->left = _del( value , r->left);
return r ;
}else{
r->right =_del( value, r->right );
return r ;
}
return r ;
}
作业:修改链表程序,能够删除全部相同元素;在指定位置后插入数据
C++ 笔记 第十六天 2007年4月12日
1、算法
脱离具体的语言
有穷性 --- 在保证执行有限步骤之后确定能够结束
确切性 --- 每条语句具体干什么
输入输出 --- 所有的算法都有输出,打印屏幕,写文件,写DB
2、快速排序法
数据个数超过一个,任选其中一个数据作为分界值,把其他数据按大小关系分为2组,分界值在中间
对两组数据实行递归重组
//快速排序算法,效率最高的排序算法。第一个参数表示数组首地址,第二个参数表示数组起始位置,第三个参数表示结束位置
void mysort( int * p , int left , int right ){
int l = left ; //从左侧开始走
int r = right ; //从右侧开始走
int povit = p[(left + right)/2]; //把数组中间的一个数据作为分界点
do{
while( p[l]<povit && l < right ){ //循环退出,则是l找到一个比自己大的
l++ ;
}
while( p[r]>povit && r > left ){
r--;
}
if( l <= r ){
int t = p[l];
p[l] = p[r];
p[r] = t ;
l++;
r--;
}
}while( l <= r ); //条件就是左右的两个人还没有碰面
if( r > left ){ //只要右边的仍比左边的大,就要继续循环
mysort( p , left , r );
}
if( l < right ){ //只要左边的仍比右边的大,也要继续循环
mysort( p , l , right );
}
}
3、直接使用系统的qsort()函数
要自己定义一个排序规则
4、模版
(1)模版的参数至少出现一次,才能确定类型
(2)只能在紧跟的函数中使用,函数声明紧跟在后面
声明多个模版类型 template<class T1 , class T2>
class关键字不能省略
(3)对于模版类型的要求,要能重载">","<","="
建议:在编码时能用一种运算符完成的操作,就不要使用多个运算符,避免多个重载
(4)用模版写的函数叫函数模版
函数模版在调用的时候确定类型的
用模版写的类叫类模版
数据类型,参数类型,函数返回类型都可以使用模版
类模版不是类,是不完整的类
类模版要在声明时用类名<int>指定,确定类型
(5)C++的泛型(模版)是编译时确定类型的 --- 效率
Java的泛型是运行时的
(6)模版类的声明和定义(多文件结构)是不能分开的
模版函数的声明和定义是可以分开的 template<class T> 在头文件和实现文件中都要出现
5、STL包含三大类,容器类(可以存储其他对象的对象),算法(一系列封装好的函数),迭代器(用于遍历操作的类)
容器可以直接存储对象,也可以存储对象的指针。成熟的程序员喜欢使用间接存储。
容器主要包括两种类型:序列类(一般是线形存储)和关联类(一般是非线性存储)。
vector ---- 数组 可变长 不提供pop_front()删除头元素的函数
list ----- 链表
(1)Vector v[1000]当越界的时候,会出现段错误
v.at(1000) 越界的时候,会抛出out_of_range的异常,在程序中捕获
v.size() 返回长度,可利用这个循环迭代
v.empty()判断容器是否为空
Iterator迭代器 : 可以做取*操作 *iterator
iter->name <=> (*iter).name
iter++
v.begin() 指向数组的开始
v.end() 指向数组最后一个元素的后面,是一个结束标志
vector<int> v1;
vector<int>::iterator it; //iterator是vector的一个内部类
for( it = v1.begin(); it != v1.end(); it++ )
cout << *it << endl;
v.insert(iter,5); //在iter所指的元素前面插入5
v.insert(iter,5,100); //在iter所指的元素前插入5个100
这样的插入操作,会造成原来的iterator失效,对起重新赋值,可以保证继续使用
(2)list
不能做at()
多了push_front(),pop_front()
iter不能做加n操作
使用于做频繁的插入删除操作
6、关联式容器
(1)map
适合根据键查找值的操作
存储上按照键值排序 ,并且key值唯一
map<int,Student> m;
Student s( 1 ,"liucy" );
m.insert( map<int,Student>::value_type(
s.getId() , s ) ) ; //创建一个pair,并存到map的第一个位置中 value_type是map的静态函数
Student s2( 4, "tangliang" );
m.insert( map<int,Student>::value_type(
s2.getId() , s ) ) ;
map<int,Student>::iterator it ;
for(it=m.begin();it!=m.end();it++ ){
cout<< it->first << " "<<it->second;
cout<<endl ;
}
在map中用[]查询,并不安全
m.find(1); // 查询key为1的value
返回一个iter,指向找到的那个键值对,如果没找到,iter会与iter.end()的值相等
(2)multimap
其中的key允许重复
查找:multimap<int ,Student>::iterator it ;
multimap<int ,Student>::iterator lt ;
multimap<int ,Student>::iterator ut ;
lt = m.lower_bound( 1 );
ut = m.upper_bound( 1 );
for( it=lt ; it != ut ; it++ ){
cout<<it->first <<" " ;
cout<<it->second <<endl;
}
(3)set
set中不能插入重复数据,相当于map中的key
插入数据的时候不必指定位置
因为与map中的key一致,仍保留着排序的特性
(4) multiset
与vector类似,唯一不同的就是保留着排序的特性
7、模版的声明和实现都写在头文件中
/usr/local/include/c++/3.2/
(全文完)