分类

类型:
不限 游戏开发 计算机程序开发 Android开发 网站开发 笔记总结 其他
评分:
不限 10 9 8 7 6 5 4 3 2 1
原创:
不限
年份:
不限 2018 2019

技术文章列表

  • 机器学习 23 、BM25 Word2Vec

    前文链接:https://write-bug.com/article/2689.html
    word embedding什么是word embedding? 简单的来说就是将一个特征转换为一个向量。在推荐系统当中,我们经常会遇到离散特征,如userid、itemid。对于离散特征,我们一般的做法是将其转换为one-hot,但对于itemid这种离散特征,转换成one-hot之后维度非常高,但里面只有一个是1,其余都为0。这种情况下,我们的通常做法就是将其转换为embedding。
    word embedding为什么翻译成词嵌入模型?一个向量可以想象成多维空间,那么在多维空间里面找到一个点来代表这个词,相当于把这个词嵌入到了多维空间里,所以可以称为词嵌入模型。
    one-hot编码形式
    输入:
    apple on a apple tree – Bag of Words: ["apple", "on", "a", "tree"]– Word Embedding的输出就是每个word的向量表示
    最简单直接的是one-hot编码形式:那么每个word都对应了一种数值表示
    例如,apple对应的vector就是[1, 0, 0, 0],a对应的vector就是[0, 0, 1, 0]
    两类表达方式:基于频率:(本质:one-hot)
    Count vector:需要穷举所有单词,形成非常庞大、稀疏的矩阵,穷举时,计算每词的词频TF
    每个文档用词向量的组合来表示,每个词的权重用其出现的次数来表示
    假设语料库内容如下:
    – D1: He is a boy. – D2: She is a girl, good girl.那么可以构建如下2 × 7维的矩阵

    tfidf vector:同时考虑了TF与IDF
    IDF=log(N/n)。 – N代表语料库中文档的总数 – n代表某个word在几个文档中出现过;
    Co-Occurrence vector:只考虑滑动窗口内单词的共现,等于考虑了上下文之间的关系
    前面两类方法,每个字都是独立的,缺少了语义的学习
    例如: He is not lazy. He is intelligent. He is smart.
    如果Context Window大小为2,那么可以得到如下的共现矩阵:

    he和is计算方法(窗口共现):

    基于模型:
    CBOW
    Skip-Gram
    技术升级TF-IDF-》BM25
    Co-Occurrence-》WordtoVector
    BM25:定位:搜索引擎相关性评分
    通常我们在搜索信息时,搜索的句子为query,关联出的排序列表为item,即:
    query -》item
    那么可分词query-》token1 token2 token3
    同时可把item -》doc-》token token token
    词在文章中的评分:token1 -》doc :score1(tfidf)
    所以相关性评分query-》item:score1+score2+…
    公式可表示为:
    S(q,d)=∑Wi*R(qi,d)Wi用idf表示:

    N为索引中的全部文档数
    n(qi)为包含了qi的文档数
    相关性评分R:


    k1,k2,b为调节因子,一般k1=2,b=0.75
    b是调整文档长度对相关性影响的大小 • b越大,文档长度影响R越大,反之越小


    fi为qi在d中的出现频率 =TF
    qfi为qi在Query中的出现频率 =query中的TF

    在之前的tfidf中TF越大最后的分数也越大,而通常情况下,文章越长,TF大的可能性越大,没有考虑到文章长度的问题,这里BM25考虑到了这个问题,如果文章过长就要给定一定的打压,那么如何界定文章长度呢?除以avgdl,即所有文章平均长度

    dl为文档d的长度
    avgdl为所有文档的平均长度

    公式简化:
    绝大部分情况下,qi在Query中只会出现一次,即qfi=1


    BM25实践:
    1.gensim word2vec
    语料库-》每个词的50维向量即word embedding
    from gensim.models import word2vecmodel=word2vec.Word2Vec(sentences,size=50,window=5,min_count=1,workers=6)model.wv.save_word2vec_format(file_voc, binary=False)2.coumpute_idf
    每个词的idf
    file_corpus='../data/file_corpus.txt'file_voc='../data/voc.txt'file_idf='../data/idf.txt'class ComIdf(object): def __init__(self,file_corpus,file_voc,file_idf): self.file_corpus=file_corpus self.file_voc=file_voc self.file_idf=file_idf self.voc=load_voc(self.file_voc) self.corpus_data=self.load_corpus() self.N=len(self.corpus_data) def load_corpus(self): input_data = codecs.open(self.file_corpus, 'r', encoding='utf-8') return input_data.readlines() def com_idf(self,word): n = 0 for _,line in enumerate(self.corpus_data): n+=line.count(word) idf=math.log(1.0*self.N/n+1) return {word:idf} def parts(self): words=set(self.voc.keys()) multiprocessing.freeze_support() cores=multiprocessing.cpu_count() pool=multiprocessing.Pool(processes=cores-2) reuslt=pool.map(self.com_idf,words) idf_dict=dict() for r in reuslt: k=list(r.keys())[0] v=list(r.values())[0] idf_dict[k]=idf_dict.get(k,0)+v with codecs.open(self.file_idf,'w',encoding='utf-8') as f:f.write(json.dumps(idf_dict,ensure_ascii=False,indent=2,sort_keys=False))if __name__ == '__main__': t1 = time.time() IDF=ComIdf(file_corpus,file_voc,file_idf)IDF.parts()3.get_sentence
    分割句子,取出最常见句子1w个
    def get_sentence(): file_corpus=codecs.open('../data/file_corpus.txt','r',encoding='utf-8') file_sentence=codecs.open('../data/file_sentence.txt','w',encoding='utf-8') st=dict() for _,line in enumerate(file_corpus): line=line.strip() blocks=re_han.split(line) for blk in blocks: if re_han.match(blk) and len(blk)>10: st[blk]=st.get(blk,0)+1 st=sorted(st.items(),key=lambda x:x[1],reverse=True) for s in st[:10000]: file_sentence.write(s[0]+'\n') file_corpus.close() file_sentence.close()get_sentence()4.test
    各种算法计算打分:cos\idf\bm25\jaccard
    file_voc='./data/voc.txt'file_idf='./data/idf.txt'file_userdict='./data/medfw.txt'class SSIM(object): def __init__(self): t1 = time.time() self.voc=load_voc(file_voc) print("Loading word2vec vector cost %.3f seconds...\n" % (time.time() - t1)) t1 = time.time() self.idf=load_idf(file_idf) print("Loading idf data cost %.3f seconds...\n" % (time.time() - t1)) jieba.load_userdict(file_userdict) def M_cosine(self,s1,s2): s1_list=jieba.lcut(s1) s2_list=jieba.lcut(s2) v1=np.array([self.voc[s] for s in s1_list if s in self.voc]) v2=np.array([self.voc[s] for s in s2_list if s in self.voc]) v1=v1.sum(axis=0) v2=v2.sum(axis=0) sim=1-spatial.distance.cosine(v1,v2) return sim def M_idf(self,s1, s2): v1, v2 = [], [] s1_list = jieba.lcut(s1) s2_list = jieba.lcut(s2) for s in s1_list: idf_v = self.idf.get(s, 1) if s in self.voc: v1.append(1.0 * idf_v * self.voc[s]) for s in s2_list: idf_v = self.idf.get(s, 1) if s in self.voc: v2.append(1.0 * idf_v * self.voc[s]) v1 = np.array(v1).sum(axis=0) v2 = np.array(v2).sum(axis=0) sim = 1 - spatial.distance.cosine(v1, v2) return sim def M_bm25(self,s1, s2, s_avg=10, k1=2.0, b=0.75): bm25 = 0 s1_list = jieba.lcut(s1) for w in s1_list: idf_s = self.idf.get(w, 1) bm25_ra = s2.count(w) * (k1 + 1) bm25_rb = s2.count(w) + k1 * (1 - b + b * len(s2) / s_avg) bm25 += idf_s * (bm25_ra / bm25_rb) return bm25 def M_jaccard(self,s1, s2): s1 = set(s1) s2 = set(s2) ret1 = s1.intersection(s2) ret2 = s1.union(s2) jaccard = 1.0 * len(ret1)/ len(ret2) return jaccard def ssim(self,s1,s2,model='cosine'): if model=='idf': f_ssim=self.M_idf elif model=='bm25': f_ssim=self.M_bm25 elif model=='jaccard': f_ssim=self.M_jaccard else: f_ssim = self.M_cosine sim=f_ssim(s1,s2) return simsm=SSIM()ssim=sm.ssimdef test(): test_data=[u'临床表现及实验室检查即可做出诊断', u'面条汤等容易消化吸收的食物为佳', u'每天应该摄入足够的维生素A', u'视患者情况逐渐恢复日常活动', u'术前1天开始预防性运用广谱抗生素'] model_list=['cosine','idf','bm25','jaccard'] file_sentence=codecs.open('./data/file_sentence.txt','r',encoding='utf-8') train_data=file_sentence.readlines() for model in model_list: t1 = time.time() dataset=dict() result=dict() for s1 in test_data: dataset[s1]=dict() for s2 in train_data: s2=s2.strip() if s1!=s2: sim=similarity.ssim(s1,s2,model=model) dataset[s1][s2]=dataset[s1].get(s2,0)+sim for r in dataset: top=sorted(dataset[r].items(),key=lambda x:x[1],reverse=True) result[r]=top[0] with codecs.open('./data/test_result.txt','a') as f: f.write('--------------The result of %s method------------------\n '%model) f.write('\tThe computing cost %.3f seconds\n'% (time.time() - t1)) f.write(json.dumps(result, ensure_ascii=True, indent=2, sort_keys=False)) f.write('\n\n') file_sentence.close()test()word2vec继承了之前的窗口滑动思想,得到更好的语义理解
    涉及的技术点:
    – Hierarchical softmax分层softmax
    – Negative sampling负采样
    两种方式:

    CBOW:根据前后词,预测中间词
    Skip-Gram :根据中间词预测前后词,工业用的更多

    CBOW一句话包含了很多单词,时间窗口选择到了一个中间词,w3,假设左边右边各学习两个,中间词假设不知道,那么根据w1245猜测中间这个w3词,故意装作w3不知道做预测,那么w3就是中心词label,w1245就是周围词训练数据,随着时间窗口滑动,同理w4又继续成为了中间词,那么由此类推形成了很多训练样本


    Input layer:由one-hot编码的输入上下文{x1,…,xC}组成, 其中窗口大小为C,词汇表大小为V
    Hidden layer:隐藏层是N维的向量
    Output layer:被one-hot编码的输出单词y

    被one-hot编码的输入向量通过一个V×N维的权重矩阵W连接到隐藏层; 隐藏层通过一个N×V的权重矩阵W′连接到输出层
    不管输入多少词,相加并都映射为中间得隐层向量上,再做一个softmax输出
    Skip-Gram:CBOW的网络翻转
    样本怎么得到的?

    input word和output word都是one-hot编码的向量。最终模型的输出是一个概率分布

    softmax分类需要所有单词:这里是50002个单词,需要遍历一遍,那么每次计算都需要一定时间,不管是多次还是一次时间成本都很高,所以引出了分层softmax:
    Hierarchical softmax哈夫曼树

    是二叉树的一种特殊形式,又称为最优二叉树,其主要作用在于数据压缩和编码长度的优化
    定义:给定n个权值作为n个叶子结点,构造一棵二叉树,若带权路径长度(WPL)达到最小,称这样的二叉 树为最优二叉树,也称为霍夫曼树(Huffman Tree)
    WPL:树的带权路径长度,所有叶子结点的带权路径长度之和

    让wpl最小,压缩数据信息
    主要用于编码:从根节点出发,向左为0,向右为1
    等长编码和哈夫曼编码


    生成:
    每个节点按权重大小从小到大排序
    两个最小的为叶子节点相加成为父节点并和其他最小的成两个叶子节点,最终形成哈夫曼树
    之前做softmax,50002个单词需要从头到尾计算一遍,把词表做成树,每个叶子节点都是一个单词,树的每个结点都是一个sigmoid,来了一个单词做一个lr二分类向左还是向右分类直至最后叶子节点为所要的词
    Negative sampling负采样
    思想:每次让一个训练样本仅更新一小部分权重
    需要离错误的样本更远

    不对所有输出神经元对应的权值进行更新,只是随机选取几个“negative”词,更新他们对应的权重。当然, 我们也会更新”positive”的对应权值
    随机取的样本只要不共现就是负样本,概率大的会多选,即出现多的单词自然被取出的概率越大
    实际论文实现:


    CBOW:input是context(周围词)而output是中心词,训练过程中其实是在从output的loss学习周围词的信息 也就是embedding,但是在中间层是average的,一共预测V(vocab size)次就够了
    一次softmax

    Skipgram:用中心词预测周围词,预测的时候是一对word pair,等于对每一个中心词都有K个词作为output, 对于一个词的预测有K次,所以能够更有效的从context中学习信息,但是总共预测K*V词
    多次softmax

    Skip-Gram的训练时间更长,更准确
    Cbow训练速度快,Skip-Gram更好处理生僻字

    Word2Vec实践:
    import sysimport torchimport torch.nn as nnimport torch.nn.functional as Fimport torch.utils.data as tudfrom torch.nn.parameter import Parameterfrom collections import Counterimport numpy as npimport randomimport mathimport pandas as pdimport scipyimport sklearnfrom sklearn.metrics.pairwise import cosine_similarity# 超参数K = 100 # number of negative samplesC = 3 # nearby words thresholdNUM_EPOCHS = 2 # The number of epochs of trainingMAX_VOCAB_SIZE = 30000 # the vocabulary sizeBATCH_SIZE = 128 # the batch sizeLEARNING_RATE = 0.2 # the initial learning rateEMBEDDING_SIZE = 100# 分词def word_tokenize(text): return text.split()with open("./data/text8", "r") as fin: text = fin.read()text = [w for w in word_tokenize(text.lower())]print(text[1])vocab = dict(Counter(text).most_common(MAX_VOCAB_SIZE - 1))#计数器,取前30000个高频元素vocab["<unk>"] = len(text) - np.sum(list(vocab.values()))print (np.sum(list(vocab.values())))print (len(text))print (vocab["<unk>"])print (len(vocab))#output:originated170052071700520769010330000idx_to_word = [word for word in vocab.keys()]word_to_idx = {word: i for i, word in enumerate(idx_to_word)}word_counts = np.array([count for count in vocab.values()], dtype=np.float32)word_freqs = word_counts / np.sum(word_counts)word_freqs = word_freqs ** (3. / 4.)word_freqs = word_freqs / np.sum(word_freqs) # 用来做 negative samplingVOCAB_SIZE = len(idx_to_word)print(VOCAB_SIZE)#text 每个单词#word_to_idx 单词id化:dict#idx_to_word 每个单词#word_freqs 负采样概率#word_counts 单词计数列表#数据样本列举:class WordEmbeddingDataset(tud.Dataset): def __init__(self, text, word_to_idx, idx_to_word, word_freqs, word_counts): ''' text: a list of words, all text from the training dataset word_to_idx: the dictionary from word to idx idx_to_word: idx to word mapping word_freq: the frequency of each word word_counts: the word counts ''' super(WordEmbeddingDataset, self).__init__() self.text_encoded = [word_to_idx.get(t, VOCAB_SIZE - 1) for t in text] self.text_encoded = torch.Tensor(self.text_encoded).long() self.word_to_idx = word_to_idx self.idx_to_word = idx_to_word self.word_freqs = torch.Tensor(word_freqs) self.word_counts = torch.Tensor(word_counts) def __len__(self): ''' 返回整个数据集(所有单词)的长度 ''' return len(self.text_encoded) def __getitem__(self, idx): ''' 这个function返回以下数据用于训练 - 中心词 - 这个单词附近的(positive)单词 - 随机采样的K个单词作为negative sample ''' center_word = self.text_encoded[idx] pos_indices = list(range(idx - C, idx)) + list(range(idx + 1, idx + C + 1)#窗口左3右3 pos_indices = [i % len(self.text_encoded) for i in pos_indices]#去除类似第一次左边无词这种情况 pos_words = self.text_encoded[pos_indices] neg_words = torch.multinomial(self.word_freqs, K * pos_words.shape[0], True) return center_word, pos_words, neg_wordsdataset = WordEmbeddingDataset(text, word_to_idx, idx_to_word, word_freqs, word_counts)dataloader = tud.DataLoader(dataset, batch_size=BATCH_SIZE, shuffle=True, num_workers=4)
    class EmbeddingModel(nn.Module): def __init__(self, vocab_size, embed_size): ''' 初始化输出和输出embedding ''' super(EmbeddingModel, self).__init__() self.vocab_size = vocab_size self.embed_size = embed_size initrange = 0.5 / self.embed_size self.out_embed = nn.Embedding(self.vocab_size, self.embed_size, sparse=False) self.out_embed.weight.data.uniform_(-initrange, initrange) self.in_embed = nn.Embedding(self.vocab_size, self.embed_size, sparse=False) self.in_embed.weight.data.uniform_(-initrange, initrange) def forward(self, input_labels, pos_labels, neg_labels): ''' input_labels: 中心词, [batch_size] pos_labels: 中心词周围 context window 出现过的单词 [batch_size * (window_size * 2)] neg_labelss: 中心词周围没有出现过的单词,从 negative sampling 得到 [batch_size, (window_size * 2 * K)] return: loss ''' input_embedding = self.in_embed(input_labels) # B * embed_size pos_embedding = self.out_embed(pos_labels) # B * (2*C) * embed_size neg_embedding = self.out_embed(neg_labels) # B * (2*C * K) * embed_size print("input_embedding size:", input_embedding.size()) print("pos_embedding size:", pos_embedding.size()) print("neg_embedding size:", neg_embedding.size()) log_pos = torch.bmm(pos_embedding, input_embedding.unsqueeze(2)).squeeze() # B * (2*C) log_neg = torch.bmm(neg_embedding, -input_embedding.unsqueeze(2)).squeeze() # B * (2*C*K)先拓展再压缩 print("log_pos size:", log_pos.size()) print("log_neg size:", log_neg.size()) log_pos = F.logsigmoid(log_pos).sum(1)#1为横向压缩,2为竖向压缩,可以试试玩 log_neg = F.logsigmoid(log_neg).sum(1) loss = log_pos + log_neg print("log_pos size:", log_pos.size()) print("log_neg size:", log_neg.size()) print("loss size:", loss.size()) return -loss def input_embeddings(self): return self.in_embed.weight.data.cpu().numpy()model = EmbeddingModel(VOCAB_SIZE, EMBEDDING_SIZE)if USE_CUDA: model = model.cuda()if __name__ == '__main__': optimizer = torch.optim.SGD(model.parameters(), lr=LEARNING_RATE) for e in range(NUM_EPOCHS): for i, (input_labels, pos_labels, neg_labels) in enumerate(dataloader): print(input_labels.size()) print(pos_labels.size()) print(neg_labels.size()) input_labels = input_labels.long() pos_labels = pos_labels.long() neg_labels = neg_labels.long() if USE_CUDA: input_labels = input_labels.cuda() pos_labels = pos_labels.cuda() neg_labels = neg_labels.cuda() optimizer.zero_grad() loss = model(input_labels, pos_labels, neg_labels).mean() loss.backward() optimizer.step() if i % 100 == 0: with open(LOG_FILE, "a") as fout: fout.write("epoch: {}, iter: {}, loss: {}\n".format(e, i, loss.item())) print("epoch: {}, iter: {}, loss: {}".format(e, i, loss.item())) embedding_weights = model.input_embeddings() torch.save(model.state_dict(), "embedding-{}.th".format(EMBEDDING_SIZE))使用场景:
    1.item title检索 +ANN+word avg,word average: – 对所有输入词向量求和并取平均 – 例如:输入三个4维词向量:(1,2,3,4),(9,6,11,8),(5,10,7,12) – 那么我们word2vec映射后的词向量就是(5,6,7,8)
    2.模型初始化:处理特征等
    3.近义词(语义理解):可通过计算词向量的加减,与相似度可进行语义理解
    实践例:
    def evaluate(filename, embedding_weights): if filename.endswith(".csv"): data = pd.read_csv(filename, sep=",") else: data = pd.read_csv(filename, sep="\t") human_similarity = [] model_similarity = [] for i in data.iloc[:, 0:2].index: word1, word2 = data.iloc[i, 0], data.iloc[i, 1] if word1 not in word_to_idx or word2 not in word_to_idx: continue else: word1_idx, word2_idx = word_to_idx[word1], word_to_idx[word2] word1_embed, word2_embed = embedding_weights[[word1_idx]], embedding_weights[[word2_idx]] model_similarity.append(float(sklearn.metrics.pairwise.cosine_similarity(word1_embed, word2_embed))) human_similarity.append(float(data.iloc[i, 2])) return scipy.stats.spearmanr(human_similarity, model_similarity)# , model_similaritydef find_nearest(word): index = word_to_idx[word] embedding = embedding_weights[index] cos_dis = np.array([scipy.spatial.distance.cosine(e, embedding) for e in embedding_weights])#1-cosine return [idx_to_word[i] for i in cos_dis.argsort()[:10]]model.load_state_dict(torch.load("model/embedding-{}.th".format(EMBEDDING_SIZE), map_location='cpu'))embedding_weights = model.input_embeddings()# 找相似的单词for word in ["good", "computer", "china", "mobile", "study"]: print(word, find_nearest(word))# 单词之间的关系,寻找nearest neighborsman_idx = word_to_idx["man"]king_idx = word_to_idx["king"]woman_idx = word_to_idx["woman"]embedding = embedding_weights[woman_idx] - embedding_weights[man_idx] + embedding_weights[king_idx]cos_dis = np.array([scipy.spatial.distance.cosine(e, embedding) for e in embedding_weights])for i in cos_dis.argsort()[:20]: print(idx_to_word[i])
    1 留言 2019-06-23 14:57:53 奖励13点积分
  • 中山大学智慧健康服务平台应用开发-Broadcast使用

    一、实验题目中山大学智慧健康服务平台应用开发
    二、实现内容2.1 Broadcast 使用2.1.1 实验目的
    掌握 Broadcast 编程基础
    掌握动态注册 Broadcast 和静态注册 Broadcast
    掌握Notification 编程基础
    掌握 EventBus 编程基础

    2.1.2 实验内容在之前的基础上,实现静态广播、动态广播两种改变Notification 内容的方法。
    要求在启动应用时,会有通知产生,随机推荐一个食品

    点击通知跳转到所推荐食品的详情界面

    点击收藏图标,会有对应通知产生,并通过Eventbus在收藏列表更新数据

    点击通知返回收藏列表

    实现方式要求:启动页面的通知由静态广播产生,点击收藏图标的通知由动态广播产生。
    2.1.3 验收内容
    静态广播:启动应用是否有随机推荐食品的通知产生。点击通知是否正确跳转到所推荐食品的详情界面
    动态广播:点击收藏后是否有提示食品已加入收藏列表的通知产生。同时注意设置launchMode。点击通知是否跳转到收藏列表
    Eventbus:点击收藏列表图标是否正确添加食品到收藏列表。每点击一次,添加对应的一个食品到收藏列表并产生一条通知

    三、实验结果3.1 实验截图下图为打开app后,产生一个推荐食品的通知

    下图为点击该通知,会跳转至食物详情页面。点击收藏按钮时,产生收藏的通知

    下图为点击收藏通知,跳转至收藏列表页面

    3.2 实验步骤以及关键代码3.2.1 利用静态广播实现今日推荐功能在AndroidManifest.xml注册静态广播接受方其中StaticReceiver为类名
    <receiver android:name=".StaticReceiver"> <intent-filter> <action android:name="com.example.asus.health.MyStaticFilter" /> </intent-filter> </receiver>
    实现StaticReceiver类,重构onReceive函数其中要根据intent的action来确定是否接受该广播的内容,来实现功能,而需要实现的包括一个notification的弹出以及点击它跳转到详情页面。
    notification部分由builder的设置函数来设置名字,内容,等等,由NotificationManager来发出该notification。
    点击后跳转的功能则需要给builder设置一个ContentIntent,这个intent为PeddingIntent,即不会马上跳转,而是需要等待用户的操作。它的构造函数传递了一个普通的intent,而这个intent是携带了所需的数据来生成详情页面。
    public void onReceive(Context context, Intent intent) { if (intent.getAction().equals(STATICACTION)){ Bundle bundle = intent.getExtras(); //TODO:添加Notification部分 Notification.Builder builder = new Notification.Builder(context); //跳回主页面 Intent intent2 = new Intent(context,Details.class); Bundle bundle2 = new Bundle(); String s[] = new String [5]; s[0] = ((MyCollection)bundle.getSerializable("collect")).getName(); s[1] = ((MyCollection)bundle.getSerializable("collect")).getMaterial(); s[2] = ((MyCollection)bundle.getSerializable("collect")).getType(); s[3] = ((MyCollection)bundle.getSerializable("collect")).getContent(); s[4] = ((MyCollection)bundle.getSerializable("collect")).getIs_star()?"yes":"no"; bundle2.putStringArray("msg",s); intent2.putExtras(bundle2); PendingIntent contentIntent = PendingIntent.getActivity( context, 0, intent2, PendingIntent.FLAG_UPDATE_CURRENT); //对Builder进行配置 builder.setContentTitle("今日推荐") //设置通知栏标题:发件人 .setContentText(((MyCollection)bundle.getSerializable("collect")).getName()) //设置通知栏显示内容:短信内容 .setTicker("您有一条新消息") //通知首次出现在通知栏,带上升动画效果的 .setSmallIcon(R.mipmap.empty_star) //设置通知小ICON 空星 .setContentIntent(contentIntent) //传递内容 .setAutoCancel(true); //设置这个标志当用户单击面板就可以让通知将自动取消 //获取状态通知栏管理 NotificationManager manager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); //绑定Notification,发送通知请求 Notification notify = builder.build(); manager.notify(0,notify); } }
    在FoodList主页面onCreat时生成广播注意action的字符串要与上面的Reciver的相同,不然无法正确接受广播,随机数则是返回一个0到n-1的整数表示随机生成一个推荐食物,然后将所需数据放入intent,通过sendBroadcast函数发送该广播。
    //打开应用时,发送一个静态广播 private void boardcastforOpen(int n){ final String STATICACTION = "com.example.asus.health.MyStaticFilter"; Random random = new Random(); int num = random.nextInt(n); //返回一个0到n-1的整数 Intent intentBroadcast = new Intent(STATICACTION); //定义Intent Log.i("se",getPackageName()); Bundle bundles = new Bundle(); bundles.putSerializable("collect", data2.get(num)); intentBroadcast.putExtras(bundles); sendBroadcast(intentBroadcast); }
    3.2.2 利用动态广播实现收藏信息提示实现广播接受器DynamicReceiver类与静态Receiver的实现过程差不多,一样是实现builder,然后放置peddingIntent,这里就不再重复放代码。唯一的不同点在于,它所要跳回的是收藏夹页面,即FoodList主页面,这里要对intent设置flag,否则无法在foodlist中get到新的intent。
    //跳回收藏夹 Intent intent2 = new Intent(context,FoodList.class); Bundle bundle2 = new Bundle(); bundle2.putString("tag","collect"); intent2.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP); intent2.putExtras(bundle2);
    对详情页面的收藏事件进行处理在监听器中设置发送广播的intent,当按下收藏后会发出广播,并传递参数。其中还使用了eventbus来传递收藏的数据。
    //处理收藏按钮 final ImageView collect_but = findViewById(R.id.collect); collect_but.setOnClickListener(new View.OnClickListener(){ @Override public void onClick(View v){ temp.setIs_collected(true); Toast.makeText(Details.this, "已收藏",Toast.LENGTH_SHORT).show(); EventBus.getDefault().post(new MessageEvent(temp)); //发送广播 Intent intentBroadcast = new Intent(); intentBroadcast.setAction(DYNAMICACTION); sendBroadcast(intentBroadcast); } });
    在FoodList注册动态接收器以及注销动态接收器注意分别要在onCreate函数以及onDestroy函数中实现注册与注销 。
    3.2.3 使用EventBus来实现数据的传输在这一点上,要改进上一周实验的代码,不再需要点击返回按钮利用setResult以及onActivityResult两个函数来返回信息。而是通过eventbus的订阅发布模式。
    在FoodList来注册订阅者,订阅消息。而在Detail来发布信息。其中onMessageEvent函数用于收到发布消息后,来调用之前的接口函数刷新列表。
    发布消息就是上面点击收藏按钮后 EventBus.getDefault().post(new MessageEvent(temp));
    //注册订阅者(注册收藏列表所在Activity为订阅者) EventBus.getDefault().register(this); @Subscribe(threadMode = ThreadMode.MAIN) public void onMessageEvent(MessageEvent event) { Log.i("hello","this is eventbus."); MyCollection mc = event.getCollection(); refreshList(mc,simpleAdapter); }
    3.2.4 从详情跳转回收藏夹这里由于收藏夹FoodList为经常返回的页面,故这里使用了android:launchMode=”singleInstance”,即不让它重复创建新的活动。
    所以再get我的返回intent时是拿不到新的intent的,这里需要重写onNewIntent函数,而且接收新的intent要在onResume中。
    这里要求要显示收藏夹页面,所以要将食物列表隐藏起来。
    @Override protected void onNewIntent(Intent intent){ super.onNewIntent(intent); setIntent(intent); } @Override protected void onResume(){ super.onResume(); //处理跳转 Bundle bundle=this.getIntent().getExtras(); if(bundle != null) { String str = bundle.getString("tag"); Log.i("resume2",str); if (str.equals("collect")) { findViewById(R.id.recyclerView).setVisibility(View.GONE); findViewById(R.id.listView).setVisibility(View.VISIBLE);//设置Favourite可见 tag_for_foodlist = false; f_but.setImageResource(R.mipmap.mainpage); } } }
    3.3 实验遇到的困难以及解决思路3.3.1 在安卓8.0版本中无法使用静态接收器,发送广播后,无法成功接收方法一:解决这个问题,需要给receiver设置component,给予它的包名以及类名。
    intent.setComponent(new ComponentName(getPackageName(),getPackageName()+".xxxxReceiver"));
    方法二:下载新的虚拟机,使用安卓7.0版本,则可以顺利接收静态广播。
    3.3.2 使用EventBus时候,FoodList主页面无法得到post的信息我按部就班地在Detail页面收藏按钮post,在FoodList订阅消息却毫无反应。首先,我认为是我的接收函数写错了,没有订阅到信息。通过Log.i发现确实没有进入到onMessageEvent函数中,于是对这个问题进行了查阅。网上有推荐使用stickyPost的,怀疑原因出在信息接收发生在创建接收者之前,但显然与函数执行顺序不符,它是先来到了主页面,所以必然创建了receiver。
    经过大半个小时的查找发现是,post传的参数错误,并没有生成MessageEvent,而是错误地直接传递了数据包。
    //错误EventBus.getDefault().post(temp);//正确EventBus.getDefault().post(new MessageEvent(temp));
    3.3.3 从收藏通知返回主页面时候,出现无法拿到intent的情况由于我是在动态接收方的builder绑定了Peddingintent,当点击通知,应该要返回这个intent到主页面,然而主页面所获取的intent是空值。这一点让我怀疑了很久,问了同学才得知,这是声明了singleInstance的问题。
    比如说在一个应用中A activity 跳转至 B activity 在跳转至 C activity 然后C做了一定的操作之后再返回A 界面。这样在A activity的启动模式设置为singleTask后。C界面跳转至A界面时,就会去判断栈内是否有改Activity实例,如果有就直接执行A界面的onNewIntent()方法,我们就可以把逻辑处理放在改生命周期方法中,如果没有就会走Activity的oncrate方法去创建实例。
    所以这里需要重写onNewIntent来获取新的intent,而不是直接传递旧intent导致错误。
    @Override protected void onNewIntent(Intent intent){ super.onNewIntent(intent); setIntent(intent); }
    四、实验思考及感想这次实验需要在安卓8.0与安卓7.0之间权衡,有些属性方法已经在8.0版本出现了变化,所以当使用错误,出现奇怪的现象时,第一步先检查自己的代码逻辑有否问题,第二步就是要查阅是否存在版本的兼容性问题产生了这些错误。这次作业就是如此,关于广播的实现,个人还是喜欢动态广播,不需要再静态注册在manifest中,代码也更加简便。
    对于不同活动之间的传输,使用EventBus比之前的intent更加方便,减轻了耦合性,不用经常记住,哪个intent返回哪里,所以这次我也修改了不少前面实验使用intent的代码。除此之外,充分理解信息传输还需要理解一下活动的存活过程,什么时候调用onCreat,什么时候使用onResume。
    2 留言 2019-07-17 23:38:40 奖励17点积分
  • 基于WinInet实现的HTTP文件下载 精华

    背景之前写过的网络数据传输的小程序都是基于Socket去写的,所以,如果要用Socket传输数据到网站,还需要根据域名获取服务器的IP地址,然后再建立连接,传输数据。虽然,Socket也可以实现网络传输,但是,总感觉不是很方便。所以,后来随着知识面的拓展,了解到Windows还专门提供了WinInet网络库,封装了比较简便的接口,去实现HTTP和FTP等传输协议的数据传输。
    本文就是基于WinInet网络库,实现通过HTTP传输协议下载文件功能的小程序。现在,就把开发过程的思路和编程分享给大家。
    主要函数介绍介绍HTTP下载文件使用到的主要的WinInet库中的API函数。
    1. InternetOpen介绍
    函数声明
    HINTERNET InternetOpen(In LPCTSTR lpszAgent,In DWORD dwAccessType,In LPCTSTR lpszProxyName,In LPCTSTR lpszProxyBypass,In DWORD dwFlags);
    参数lpszAgent指向一个空结束的字符串,该字符串指定调用WinInet函数的应用程序或实体的名称。使用此名称作为用户代理的HTTP协议。dwAccessType指定访问类型,参数可以是下列值之一:



    Value
    Meaning




    INTERNET_OPEN_TYPE_DIRECT
    使用直接连接网络


    INTERNET_OPEN_TYPE_PRECONFIG
    获取代理或直接从注册表中的配置,使用代理连接网络


    INTERNETOPEN_TYPE_PRECONFIG WITH_NO_AUTOPROXY
    获取代理或直接从注册表中的配置,并防止启动Microsoft JScript或Internet设置(INS)文件的使用


    INTERNET_OPEN_TYPE_PROXY
    通过代理的请求,除非代理旁路列表中提供的名称解析绕过代理,在这种情况下,该功能的使用



    lpszProxyName指针指向一个空结束的字符串,该字符串指定的代理服务器的名称,不要使用空字符串;如果dwAccessType未设置为INTERNET_OPEN_TYPE_PROXY,则此参数应该设置为NULL。
    lpszProxyBypass指向一个空结束的字符串,该字符串指定的可选列表的主机名或IP地址。如果dwAccessType未设置为INTERNET_OPEN_TYPE_PROXY的 ,参数省略则为NULL。
    dwFlags参数可以是下列值的组合:



    VALUE
    MEANING




    INTERNET_FLAG_ASYNC
    使异步请求处理的后裔从这个函数返回的句柄


    INTERNET_FLAG_FROM_CACHE
    不进行网络请求,从缓存返回的所有实体,如果请求的项目不在缓存中,则返回一个合适的错误,如ERROR_FILE_NOT_FOUND


    INTERNET_FLAG_OFFLINE
    不进行网络请求,从缓存返回的所有实体,如果请求的项目不在缓存中,则返回一个合适的错误,如ERROR_FILE_NOT_FOUND



    返回值成功:返回一个有效的句柄,该句柄将由应用程序传递给接下来的WinInet函数。失败:返回NULL。

    2. InternetConnect介绍
    函数声明
    HINTERNET WINAPI InternetConnect( HINTERNET hInternet, LPCTSTR lpszServerName, INTERNET_PORT nServerPort, LPCTSTR lpszUserName, LPCTSTR lpszPassword, DWORD dwService, DWORD dwFlags, DWORD dwContext);
    参数说明hInternet:由InternetOpen返回的句柄。lpszServerName:连接的ip或者主机名nServerPort:连接的端口。lpszUserName:用户名,如无置NULL。lpszPassword:密码,如无置NULL。dwService:使用的服务类型,可以使用以下

    INTERNET_SERVICE_FTP = 1:连接到一个 FTP 服务器上INTERNET_SERVICE_GOPHER = 2INTERNET_SERVICE_HTTP = 3:连接到一个 HTTP 服务器上
    dwFlags:文档传输形式及缓存标记。一般置0。dwContext:当使用回叫信号时, 用来识别应用程序的前后关系。返回值成功返回非0。如果返回0。要InternetCloseHandle释放这个句柄。

    3. HttpOpenRequest介绍
    函数声明
    HINTERNET HttpOpenRequest( _In_ HINTERNET hConnect, _In_ LPCTSTR lpszVerb, _In_ LPCTSTR lpszObjectName, _In_ LPCTSTR lpszVersion, _In_ LPCTSTR lpszReferer, _In_ LPCTSTR *lplpszAcceptTypes, _In_ DWORD dwFlags, _In_ DWORD_PTR dwContext);
    参数
    hConnect:由InternetConnect返回的句柄。
    lpszVerb:一个指向某个包含在请求中要用的动词的字符串指针。如果为NULL,则使用“GET”。
    lpszObjectName:一个指向某个包含特殊动词的目标对象的字符串的指针。通常为文件名称、可执行模块或者查找标识符。
    lpszVersion:一个指向以null结尾的字符串的指针,该字符串包含在请求中使用的HTTP版本,Internet Explorer中的设置将覆盖该参数中指定的值。如果此参数为NULL,则该函数使用1.1或1.0的HTTP版本,这取决于Internet Explorer设置的值。
    lpszReferer:一个指向指定了包含着所需的URL (pstrObjectName)的文档地址(URL)的指针。如果为NULL,则不指定HTTP头。
    lplpszAcceptTypes:一个指向某空终止符的字符串的指针,该字符串表示客户接受的内容类型。如果该字符串为NULL,服务器认为客户接受“text/*”类型的文档 (也就是说,只有纯文本文档,并且不是图片或其它二进制文件)。内容类型与CGI变量CONTENT_TYPE相同,该变量确定了要查询的含有相关信息的数据的类型,如HTTP POST和PUT。
    dwFlags:dwFlags的值可以是下面一个或者多个。



    价值
    说明




    INTERNET_FLAG_DONT_CACHE
    不缓存的数据,在本地或在任何网关。 相同的首选值INTERNET_FLAG_NO_CACHE_WRITE。


    INTERNET_FLAG_EXISTING_CONNECT
    如果可能的话,重用现有的连接到每个服务器请求新的请求而产生的InternetOpenUrl创建一个新的会话。 这个标志是有用的,只有对FTP连接,因为FTP是唯一的协议,通常在同一会议执行多个操作。 在Win 32 API的缓存一个单一的Internet连接句柄为每个HINTERNET处理产生的InternetOpen。


    INTERNET_FLAG -超链接
    强制重载如果没有到期的时间也没有最后修改时间从服务器在决定是否加载该项目从网络返回。


    INTERNET_FLAG_IGNORE_CERT_CN_INVALID
    禁用的Win32上网功能的SSL /厘为基础的打击是从给定的请求服务器返回的主机名称证书检查。 Win32的上网功能用来对付证书由匹配主机名和HTTP请求一个简单的通配符规则比较简单的检查。


    INTERNET_FLAG_IGNORE_CERT_DATE_INVALID
    禁用的Win32上网功能的SSL /厘为基础的HTTP请求适当的日期,证书的有效性检查。


    INTERNET_FLAG_IGNORE_REDIRECT_TO_HTTP
    禁用的Win32上网功能能够探测到这种特殊类型的重定向。 当使用此标志,透明的Win32上网功能允许对HTTP重定向的URL从HTTPS。


    INTERNET_FLAG_IGNORE_REDIRECT_TO_HTTPS
    禁用的Win32上网功能能够探测到这种特殊类型的重定向。 当使用此标志,透明的Win32上网功能允许从HTTP重定向到HTTPS网址。


    INTERNET_FLAG_KEEP_CONNECTION
    使用保持活动语义,如果有的话,给HTTP请求连接。 这个标志是必需的微软网络(MSN),NT LAN管理器(NTLM)和其他类型的身份验证。


    INTERNET_FLAG_MAKE_PERSISTENT
    不再支持。


    INTERNET_FLAG_MUST_CACHE_REQUEST
    导致一个临时文件如果要创建的文件不能被缓存。 相同的首选值INTERNET_FLAG_NEED_FILE。


    INTERNET_FLAG_NEED_FILE
    导致一个临时文件如果要创建的文件不能被缓存。


    INTERNET_FLAG_NO_AUTH
    不尝试HTTP请求身份验证自动。


    INTERNET_FLAG_NO_AUTO_REDIRECT
    不自动处理HTTP请求重定向只。


    INTERNET_FLAG_NO_CACHE_WRITE
    不缓存的数据,在本地或在任何网关。


    INTERNET_FLAG_NO_COOKIES
    不会自动添加Cookie标头的请求,并不会自动添加返回的Cookie的HTTP请求的Cookie数据库。


    INTERNET_FLAG_NO_UI
    禁用cookie的对话框。


    INTERNET_FLAG_PASSIVE
    使用被动FTP语义FTP文件和目录。


    INTERNET_FLAG_RAW_DATA
    返回一个数据WIN32_FIND_DATA结构时,FTP目录检索信息。 如果这个标志,或者未指定代理的电话是通过一个CERN,InternetOpenUrl返回的HTML版本的目录。


    INTERNET_FLAG_PRAGMA_NOCACHE
    强制要求被解决的原始服务器,即使在代理缓存的副本存在。


    INTERNET_FLAG_READ_PREFETCH
    该标志目前已停用。


    INTERNET_FLAG_RELOAD
    从导线获取数据,即使是一个本地缓存。


    INTERNET_FLAG_RESYNCHRONIZE
    重整HTTP资源,如果资源已被修改自上一次被下载。 所有的FTP资源增值。


    INTERNET_FLAG_SECURE
    请确保在使用SSL或PCT线交易。 此标志仅适用于HTTP请求。



    dwContext:OpenRequest操作的上下文标识符。

    4. InternetReadFile介绍
    函数声明
    BOOL InternetReadFile( __in HINTERNET hFile,__out LPVOID lpBuffer,__in DWORD dwNumberOfBytesToRead,__out LPDWORD lpdwNumberOfBytesRead);
    参数

    hFile[in]
    由InternetOpenUrl,FtpOpenFile, 或HttpOpenRequest函数返回的句柄.
    lpBuffer[out]
    缓冲器指针
    dwNumberOfBytesToRead[in]
    欲读数据的字节量。
    lpdwNumberOfBytesRead[out]
    接收读取字节量的变量。该函数在做任何工作或错误检查之前都设置该值为零

    返回值成功:返回TRUE,失败,返回FALSE

    程序设计原理该部分讲解下程序设计的原理以及实现的流程,让大家有个宏观的认识。原理是:

    首先,使用 InternetCrackUrl 函数分解URL,从URL中提取网站的域名、路径以及URL的附加信息等。关于 InternetCrackUrl 分解URL的介绍和实现,可以参考本站上的的 “URL分解之InternetCrackUrl” 这篇文章
    使用 InternetOpen 建立会话,获取会话句柄
    使用 InternetConnect 与网站建立连接,获取连接句柄
    设置HTTP的访问标志,使用 HttpOpenRequest 打开HTTP的“GET”请求
    使用 HttpSendRequest 发送访问请求
    根据返回的Response Header的数据中,获取将要接收数据的长度
    使用 InternetReadFile 接收数据
    关闭句柄,释放资源

    其中,上面的 8 个步骤中,要注意的就是第 6 步,获取返回的数据长度,是从响应信息头中的获取“Content-Length: ”(注意有个空格)这个字段的数据。
    编程实现1. 导入WinInet库#include <WinInet.h>#pragma comment(lib, "WinInet.lib")
    2. HTTP文件下载编程实现// 数据下载// 输入:下载数据的URL路径// 输出:下载数据内容、下载数据内容长度BOOL Http_Download(char *pszDownloadUrl, BYTE **ppDownloadData, DWORD *pdwDownloadDataSize){ // INTERNET_SCHEME_HTTPS、INTERNET_SCHEME_HTTP、INTERNET_SCHEME_FTP等 char szScheme[MAX_PATH] = { 0 }; char szHostName[MAX_PATH] = { 0 }; char szUserName[MAX_PATH] = { 0 }; char szPassword[MAX_PATH] = { 0 }; char szUrlPath[MAX_PATH] = { 0 }; char szExtraInfo[MAX_PATH] = { 0 }; ::RtlZeroMemory(szScheme, MAX_PATH); ::RtlZeroMemory(szHostName, MAX_PATH); ::RtlZeroMemory(szUserName, MAX_PATH); ::RtlZeroMemory(szPassword, MAX_PATH); ::RtlZeroMemory(szUrlPath, MAX_PATH); ::RtlZeroMemory(szExtraInfo, MAX_PATH); // 分解URL if (FALSE == Http_UrlCrack(pszDownloadUrl, szScheme, szHostName, szUserName, szPassword, szUrlPath, szExtraInfo, MAX_PATH)) { return FALSE; } // 数据下载 HINTERNET hInternet = NULL; HINTERNET hConnect = NULL; HINTERNET hRequest = NULL; DWORD dwOpenRequestFlags = 0; BOOL bRet = FALSE; unsigned char *pResponseHeaderIInfo = NULL; DWORD dwResponseHeaderIInfoSize = 2048; BYTE *pBuf = NULL; DWORD dwBufSize = 64 * 1024; BYTE *pDownloadData = NULL; DWORD dwDownloadDataSize = 0; DWORD dwRet = 0; DWORD dwOffset = 0; do { // 建立会话 hInternet = ::InternetOpen("WinInetGet/0.1", INTERNET_OPEN_TYPE_PRECONFIG, NULL, NULL, 0); if (NULL == hInternet) { Http_ShowError("InternetOpen"); break; } // 建立连接 hConnect = ::InternetConnect(hInternet, szHostName, INTERNET_DEFAULT_HTTP_PORT, szUserName, szPassword, INTERNET_SERVICE_HTTP, 0, 0); if (NULL == hConnect) { Http_ShowError("InternetConnect"); break; } // 打开并发送HTTP请求 dwOpenRequestFlags = INTERNET_FLAG_IGNORE_REDIRECT_TO_HTTP | INTERNET_FLAG_KEEP_CONNECTION | INTERNET_FLAG_NO_AUTH | INTERNET_FLAG_NO_COOKIES | INTERNET_FLAG_NO_UI; if (0 < ::lstrlen(szExtraInfo)) { // 注意此处的连接 ::lstrcat(szUrlPath, szExtraInfo); } hRequest = ::HttpOpenRequest(hConnect, "GET", szUrlPath, NULL, NULL, NULL, dwOpenRequestFlags, 0); if (NULL == hRequest) { Http_ShowError("HttpOpenRequest"); break; } // 发送请求 bRet = ::HttpSendRequest(hRequest, NULL, 0, NULL, 0); if (FALSE == bRet) { Http_ShowError("HttpSendRequest"); break; } // 接收响应的报文信息头(Get Response Header) pResponseHeaderIInfo = new unsigned char[dwResponseHeaderIInfoSize]; if (NULL == pResponseHeaderIInfo) { break; } ::RtlZeroMemory(pResponseHeaderIInfo, dwResponseHeaderIInfoSize); bRet = ::HttpQueryInfo(hRequest, HTTP_QUERY_RAW_HEADERS_CRLF, pResponseHeaderIInfo, &dwResponseHeaderIInfoSize, NULL); if (FALSE == bRet) { Http_ShowError("HttpQueryInfo"); break; }#ifdef _DEBUG printf("[HTTP_Download_ResponseHeaderIInfo]\n\n%s\n\n", pResponseHeaderIInfo);#endif // 从 中字段 "Content-Length: "(注意有个空格) 获取数据长度 bRet = Http_GetContentLength((char *)pResponseHeaderIInfo, &dwDownloadDataSize); if (FALSE == bRet) { break; } // 接收报文主体内容(Get Response Body) pBuf = new BYTE[dwBufSize]; if (NULL == pBuf) { break; } pDownloadData = new BYTE[dwDownloadDataSize]; if (NULL == pDownloadData) { break; } ::RtlZeroMemory(pDownloadData, dwDownloadDataSize); do { ::RtlZeroMemory(pBuf, dwBufSize); bRet = ::InternetReadFile(hRequest, pBuf, dwBufSize, &dwRet); if (FALSE == bRet) { Http_ShowError("InternetReadFile"); break; } ::RtlCopyMemory((pDownloadData + dwOffset), pBuf, dwRet); dwOffset = dwOffset + dwRet; } while (dwDownloadDataSize > dwOffset); // 返回数据 *ppDownloadData = pDownloadData; *pdwDownloadDataSize = dwDownloadDataSize; } while (FALSE); // 关闭 释放 if (NULL != pBuf) { delete[]pBuf; pBuf = NULL; } if (NULL != pResponseHeaderIInfo) { delete[]pResponseHeaderIInfo; pResponseHeaderIInfo = NULL; } if (NULL != hRequest) { ::InternetCloseHandle(hRequest); hRequest = NULL; } if (NULL != hConnect) { ::InternetCloseHandle(hConnect); hConnect = NULL; } if (NULL != hInternet) { ::InternetCloseHandle(hInternet); hInternet = NULL; } return bRet;}
    程序测试在main函数中,调用上述封装好的函数,下载文件进行测试。
    main函数为:
    int _tmain(int argc, _TCHAR* argv[]){ char szHttpDownloadUrl[] = "http://www.demongan.com/source/ccc/dasanxia/520.zip"; BYTE *pHttpDownloadData = NULL; DWORD dwHttpDownloadDataSize = 0; // HTTP下载 if (FALSE == Http_Download(szHttpDownloadUrl, &pHttpDownloadData, &dwHttpDownloadDataSize)) { return 1; } // 将下载数据保存成文件 Http_SaveToFile("http_downloadsavefile.zip", pHttpDownloadData, dwHttpDownloadDataSize); // 释放内存 delete []pHttpDownloadData; pHttpDownloadData = NULL; system("pause"); return 0;}
    测试结果:
    根据返回的Response Header知道,成功下载22761460字节大小的数据。

    查看目录,有22228KB大小的“http_downloadsavefile.zip”文件成功生成,所以,数据下载成功。

    总结基于WinInet库的HTTP下载文件原理并不复杂,但是,因为涉及较多的API,每个API的执行都需要依靠上一个API成功执行返回的数据。所以,要仔细检查。如果出错,也要耐心调试,根据返回的错误码,结合程序前后部分的代码,仔细分析原因。
    参考参考自《Windows黑客编程技术详解》一书
    3 留言 2018-12-20 17:46:07 奖励13点积分
  • C语言提高笔记

    C提高一 基本概念强化1、头文件函数声明
    分文件时,头文件防止头文件重复包含
    #pragma once//兼容C++编译器//如果是C++编译器,按照C标准编译#ifdef __cplusplusextern "c"{#endif//#ifdef __cplusplus}#endif数组作为函数参数会退化为一级指针:

    数组做函数参数时,应该把数组元素个数也传递给函数;
    形参中的数组,编译器把它当做指针处理,这是C语言的特色;
    实参中的数组,和形参中数组本质不一样;

    void print_array(int a[], int n)数据类型的本质:是固定内存大小的别名
    数据类型的作用:编译器预算对象(变量)分配的内存空间大小
    int a;//告诉C编译器分配4个字节的内存数据类型可以通过typedef起别名:typedef unsigned int u32;typedef struct student{ int a; char b;}STU;可以通过sizeof()测类型大小;Void类型 (无类型)

    如果函数没有参数,定义函数时,可以用void修饰:int fun(void);
    如果函数没有返回值,必须用void修饰:void fun(int a);
    不能定义void类型的普通变量,void a;//err,无法确定类型,不同类型分配空间不一样
    可以定义void 变量:void p;//ok,32位永远是4个字节,64位8字节
    void *p;万用指针,函数返回值,函数参数

    变量的本质:内存空间的别名
    必须通过数据类型定义变量
    变量相当于门牌号,内存相当于房间,通过门牌号找到房间,通过变量找到所对应的内存
    变量的赋值:1. 直接赋值 2. 间接赋值
    int a;a=100;//直接赋值int *p=0;p=&a;//指针指向谁,就把谁的地址赋值给指针*p=22;//间接赋值重点:没有变量,哪来内存,没有内存,哪里来内存首地址
    变量三要素(名称、大小、作用域),变量的生命周期
    内存四区模型

    栈区:系统分配空间,系统自动回收,函数内部定义的变量,函数参数,函数结束,其内部变量生命周期结束;
    堆区:程序员动态分配空间,由程序员手动释放,没有手动释放,分配的空间一直可用;
    静态区(全局区):(包括全局变量和静态变量,里面又分为初始化区和未初始化区,文字常量区:字符常量):整个程序运行完毕,系统自动回收;
    代码区,内存四区模型图

    栈区地址生长方向:地址由上往下递减;堆区地址生长方向:地址由下往上递增;数组buf,buf+1地址永远递增。

    函数调用模型
    程序各个函数运行流程(压栈弹栈,入栈出栈,先进后出)
    二 指针强化指针也是一种数据类型,指针变量也是一种变量,和int a本质是一样的
    1)指针变量也是一种变量,也有空间,32位程序大小为4个字节 int *p = 0x1122; 2)*操作符,*相当于钥匙,通过*可以找到指针所指向的内存区域 int a = 10; int *p = NULL; p = &a; //指针指向谁,就把谁的地址赋值给指针 *p = 22; //*放=左边,给内存赋值,写内存 int b = *p; //*放=右边,取内存的值,读内存 3)指针变量,和指向指向的内存是两个不同的概念 char *p = NULL; char buf[] = "abcdef"; //改变指针变量的值 p = buf; p = p + 1; //改变了指向变量的值,改变了指针的指向 *p = 'm'; //改变指针指向的内存,并不会影响到指针的值 4)写内存时,一定要确保内存可写 char *buf2 = "sadgkdsjlgjlsdk"; //文字常量区,内存不可改 //buf2[2] = '1'; //err间接赋值(*p)是指针存在最大意义
    1)间接赋值三大条件 a) 两个变量 b) 建立关系 c) 通过 * 操作符进行间接赋值 1) int a; int *p; //两个变量 p = &a; //建立关系 *p = 100; //通过 * 操作符进行间接赋值 2) int b; fun(&b); //两个变量之一:实参,给函数传参时,相当于建立关系 //p = &b void fun(int *p) //两个变量之一:形参参 { *p = 100; //通过 * 操作符进行间接赋值 } 2)如何定义合适类型的指针变量 //某个变量的地址需要定义一个怎么样类型的变量保存 //在这个类型的基础上加一个* int b; int *q = &b; int **t = &q;重要:如果想通过函数形参改变实参的值,必须传地址1、值传递,形参的任何修改不会影响到实参2、地址传递,形参(通过*操作符号)的任何修改会影响到实参用1级指针形参,去间接修改了0级指针(实参)的值。用2级指针形参,去间接修改了1级指针(实参)的值。用3级指针形参,去间接修改了2级指针(实参)的值。用n级指针形参,去间接修改了n-1级指针(实参)的值。 int a = 10; fun(a); //值传递 void fun(int b) { b = 20; } fun2(&a);//地址传递 void fun2(int *p) { *p = 20; //通过*操作内存 } int *p = 0x1122; void fun3(p);//值传递 void fun3(int *p) { p = 0x2233; } void fun4(&p);//地址传递 void fun4(int **p) { *p = 0xaabb; //通过*操作内存 }3、不允许向NULL和未知非法地址拷贝内存
    char *p = NULL; //给p指向的内存区域拷贝内容 strcpy(q, "1234"); //err //静态 char buf[100]; p = buf; strcpy(q, "1234"); //ok //动态 p = (char *)malloc(sizeof(char) * 10 ); strcpy(q, "1234"); //ok char *q = "123456"; strcpy(q, "abcd"); //?4、void *指针的使用
    void *p = 0x1122; //可以这么做,不建议,一般赋值为NULL char buf[1024] = "abcd"; p = (void *)buf; //指向 char printf("p = %s\n", (char *)p); //使用时转化为实际类型指针 int a[100] = { 1, 2, 3, 4 }; p = (void *)a; //指向 int int i = 0; for (i = 0; i < 4; i++) { //使用时转化为实际类型指针 int *tmp = (int *)p; printf("%d ", *(tmp+i)); printf("%d ", tmp[i]); printf("%d ", *( (int *)p + i ) ); } void * 常用于函数参数:memset(), memcpy()5、栈区返回变量的值和变量的地址区别
    int fun() { int a = 10; return a; } int *fun2() { int a = 10; return &a; } int *fun3() { static int a = 10; return &a; } int b = fun(); //ok, b的值为10 //也ok, p确实也保存了fun2()内部a的地址 //但是,fun2完毕,a就释放,p就指向未知区域 int *p = fun2(); //ok,fun3()中的a在全局区,函数运行完毕,a的空间不释放 int *q = fun3();6、.c -> 可执行程序过程 预处理:宏定义展开、头文件展开、条件编译,这里并不会检查语法 编译:检查语法,将预处理后文件编译生成汇编文件 汇编:将汇编文件生成目标文件(二进制文件) 链接:将目标文件链接为可执行程序 程序只有在运行才加载到内存(由系统完成),但是某个变量具体分配多大,是在编译阶段就已经确定了,换句话说,在编译阶段做完处理后,程序运行时系统才知道分配多大的空间,所以,很多时候说,这个变量的空间在编译时就分配(确定)了。
    7、指针做函数参数的输入输出特性输入:主调函数分配内存输出:被调用函数分配内存//结合内存四区模型图
    main() { char buf[100] = "123456"; fun1(buf); //输入 char *p = NULL; fun2(&p); //输出 //因为p在fun2()动态分配空间了,使用完毕应该释放 if(p != NULL) { free(p); p = NULL; } } void fun1(char *p /*in*/) { strcpy(p, "1234") } void fun2(char **p /*out*/) { char *tmp = malloc(100); strcpy(tmp, "1234"); *p = tmp; //间接赋值是指针存在最大意义,通过*操作内存 }8、变量内存的值和变量的地址int a = 0;a变量内存的值为0a变量的地址(&a)绝对不为0,只要定义了变量,系统会自动为其分配空间(一个合法不为0的地址)
    三 字符串操作1、字符串基本操作 1)字符串初始化 / C语言没有字符串类型,用字符数组模拟 C语言字符串以数字0,或字符 ‘\0’ 结尾,数字 0 和字符 ‘\0’ 等价 / char str1[100] = { ‘a’, ‘b’, ‘c’, ‘d’ }; //没赋值的自动以数字0填充 char str2[] = { ‘a’, ‘b’, ‘c’, ‘d’ }; //数组长度为4,结尾没有数字0 char str4[] = “abcdef”; //常用赋值方式,栈区 char p = “abcdef”; //文字常量区,内容不允许被修改 char buf = (char *)malloc(100); //堆区 strcpy(buf, “abcd”); //“abcd”拷贝到buf指向的内存区域中
    2)sizeof和strlen区别 //使用字符串初始化,常用 char buf8[] = “abc”; //strlen: 测字符串长度,不包含数字0,字符’\0’ //sizeof:测数组长度,包含数字0,字符’\0’ printf(“strlen = %d, sizeof = %d\n”, strlen(buf8), sizeof(buf8)); 3 4 char buf9[100] = “abc”; printf(“strlen = %d, sizeof = %d\n”, strlen(buf9), sizeof(buf9)); 3 100
    3)’\0’ 后面最好别跟数字,因为几个数字合起来有可能是一个转义字符 //\012相当于\n char str[] = “\0129”; printf(“%s aaaa\n”, str);
    4)字符’\0’, 数字0, 字符’0’, NULL的区别 a) 字符’\0’ ASCII码值为 0 的字符 字符’\0’ 和 数字 0 是等级的,’\0’中’\’是转义字符 char buf[100]; //下面是等级,在数组第10个位置设置字符串结束符 buf[10] = 0; buf[10] = ‘\0’; b) 字符’0’是字符串的某个字符内容为’0’, ASCII码值为 48 的字符 char buf[100]; buf[0] = ‘0’; //第0个字符为字符 ‘0’ c) NULL 标准头文件(stdio.h)的宏 其值为数字 0
    5)数组法、指针法操作字符串 char buf[] = “abdgdgdsg” char p = buf; //buf是数组首元素地址,它也是指针 for (i = 0; i < strlen(buf); i++) { //[ ] 和 操作是等价的,也是操作指针指向内存 printf(“%c “, buf[i]); //符合程序员习惯 printf(“%c “, p[i]); //符合程序员习惯 printf(“%c “, (p+i)); printf(“%c “, (buf + i)); } 注意:数组名也是指针,数组首元素地址,但是,它是一个只读常量 p++; //ok buf++; //err
    6)字符串拷贝函数 //成功为0,失败非0 //1 判断形参指针是否为NULL //2 最好不要直接使用形参 int my_strcpy(char dst, char src) { if (dst == NULL || src == NULL) { return -1; } //辅助变量把形参接过来 char to = dst; char from = src;
    //*dst = *src //dst++, src++ //判断 *dst是否为0, 为0跳出循环 while (*to++ = *from++) ; return 0;}2、项目开发常用字符串应用模型 1、利用strstr标准库函数找出一个字符串中substr出现的个数 1)do-while模型: char p = “11abcd111122abcd333abcd3322abcd3333322qqq”; int n = 0; do { p = strstr(p, “abcd”); if (p != NULL) { n++; //累计个数 //重新设置查找的起点 p = p + strlen(“abcd”); } else //如果没有匹配的字符串,跳出循环 { break; } } while (p != 0); //如果没有到结尾
    2)while模型: char p = “11abcd22222abcd33333abcd444444qqq”; int n = 0; while( (p = strstr(p, “abcd”)) != NULL ) { //能进来,肯定有匹配的子串 //重新设置起点位置 p = p + strlen(“abcd”); n++; if(p == 0)//如果到结束符 { break; } } printf(“n = %d\n”,n);
    3)函数封装实现 int my_strstr(char p,int n) { //辅助变量 int i = 0; char tmp = p; while((tmp = strstr(tmp, “abcd”)) != NULL) { //能进来,肯定有匹配的子串 //重新设置起点位置 tmp = tmp + strlen(“abcd”); i++; if(tmp == 0)//如果到结束符 { break; } } //间接赋值 n = i; return 0; } int main(void) { char p = “11abcd22222abcd33333abcd444444qqq”; int n = 0; int ret = 0; ret = my_strstr(p,&n); if(ret != 0) { return ret; } printf(“n = %d\n”,n); return 0;}
    2、两头堵模型 char *p = “ abcddsgadsgefg “; int begin = 0; int end = strlen(p) - 1; int n = 0; if(end < 0){ return; } //从左往右移动,如果当前字符为空,而且没有结束 while (p[begin] == ‘ ‘ && p[begin] != 0) { begin++; //位置从右移动一位 } //从右往左移动,如果当前字符为空 while (p[end] == ‘ ‘) { end—; //往左移动 } n = end - begin + 1; //非空元素个数 strncpy(buf, p + begin, n); buf[n] = 0;
    //如何证明strncpy()拷贝不会自动加字符串结束符'\0'char dst[] = "aaaaaaaaaaaaaaa";strncpy(dst, "123", 3);printf("dst = %s\n", dst); //dst = "123aaaaaaaaaaaa"四 const的使用1)const声明变量为只读 //const修饰的变量,定义时初始化 const int a = 10; //a = 100; //error int q = &a;
    q = 22; char buf[100] = “abcdef”;//从左往右看,跳过类型,看修饰那个字符 //如果是修饰,说明指针指向的内存不能改变 //如果是修饰指针变量,说明指针的指向不能改变,指针的值不能修改 const char p = buf; //类似于文字常量区 char p = “123445”; char const p = buf; //修饰,指针指向能变,指针指向的内存不能变 //p[0] = ‘1’; //error p = “123456”; //ok char const p1 = buf; //修饰指针变量,指针指向的内存,指针指向不能变 //p1 = “123456”; //error p1[0] = ‘1’; //ok const char * const p2 = buf; //p2, 只读
    2)如何引用另外.c中的const变量 extern const int a;不能再赋值,只能声明
    五 多级指针1)如何定义合适类型的指针变量 //某个变量的地址需要定义一个怎么样类型的变量保存 //在这个类型的基础上加一个 int b; int q = &b; //一级指针 int t = &q; //二级指针 int *m = &t; //三级指针
    2)二级指针做输出 输入:主调函数分配内存 输出:被调用函数分配内存 char *p1 = NULL; //没有分配内存 int len = 0; getMem(&p1, &len); //要想通过函数的形参改变实参的值,必须地址传递
    void getMem(char **p1 /*out*/, int *plen /*in*/){ //间接赋值,是指针存在最大的意义。 *p1 = malloc(100); *plen = 100;} 指针做参数输出特性3)二级指针做输入的三种内存模型 1、//指针数组,数组的每个元素都是指针类型 // [] 的优先级比 高,它是数组,每个元素都是指针类型(char ) char myArray[] = {“aaaaaa”, “ccccc”, “bbbbbb”, “111111”}; //char **p = {“aaaaaa”, “ccccc”, “bbbbbb”, “111111”}; //err void fun(int a[]); void fun(int a); // a[] 等价于 a void printMyArray(char myArray[], int num); // char 代表类型,myArray[]等价于 myArray // char myArray[] -> char myArray void printMyArray(char myArray, int num); void sortMyArray(char *myArray, int num); 如果排序,交换的是指针的指向,因为原来指针指向是文字常量区,文字常量区的内存一旦分配,内存就不能变。
    2、//二维数组 10行30列,10个一维数组a[30] //总共能容量10行字符串,这个用了 4 行 //每行字符串长度不能超过29,留一个位置放结束符:数字0 char myArray[10][30] = {“aaaaaa”, “ccccc”, “bbbbbbb”, “1111111111111”}; void printMyArray(char myArray[10][30], int num); void sortMyArray(char myArray[10][30], int num); //定义二维数组,不写第一个[ ]值有条件,必须要初始化 char a[][30] = {“aaaaaa”, “ccccc”, “bbbbbbb”, “1111111111111”};//ok char a[][30]; //err,定义时必须初始化
    二维数组的数组名代表首行地址(第一行一维数组的地址)首行地址和首行首元素地址的值是一样的,但是它们步长不一样首行地址+1,跳过一行,一行30个字节,+30首行首元素地址+1,跳过一个字符,一个字符为1个字节,+1sizeof(a): 有4个一维数组,每个数组长度为30,4 * 30 = 120sizeof(a[0]): 第0个一维数组首元素地址,相当于测第0个一维数组的长度:为30char b[30];&b代表整个一维数组的地址,相当于二维数组首行地址b代表一维数组首元素地址,相当于二维数组首行首元素地址&b 和 b 的值虽然是一样,但是,它们的步长不一样&b + 1: 跳过整个数组,+30b+1: 跳过1个字符,+1//不能通过 char ** 作为函数形参,因为指针+1步长不一样// char **,指针+1步长为 4 个字节// char a[][30],指针+1步长为 1 行的长度,这里为 30 个字节void printMyArray(char **buf, int num);3、int a[3];int *q = (int *)malloc(3 * sizeof(int)); //相当于q[3]//动态分配一个数组,每个元素都是char * //char *buf[3]int n = 3;char **buf = (char **)malloc(n * sizeof(char *)); //相当于 char *buf[3]if (buf = = NULL){ return -1;} for (i = 0; i < n; i++) { buf[i] = (char )malloc(30 sizeof(char)); }
    char **myArray = NULL;char **getMem(int num); //手工打造二维数组void printMyArray(char **myArray, int num);void sortMyArray(char **myArray, int num);void arrayFree(char **myArray, int num);第三种内存模型:
    char **getMem(int n){ int i = 0; char **buf = (char **)malloc(n * sizeof(char *)); //char *buf[3] if (buf == NULL) { return NULL; } for (i = 0; i < n; i++) { buf[i] = (char *)malloc(30 * sizeof(char)); char str[30]; sprintf(str, "test%d%d", i, i); strcpy(buf[i], str); } return buf;}void print_buf(char **buf, int n){ int i = 0; for (i = 0; i < n; i++) { printf("%s, ", buf[i]); } printf("\n");}void free_buf(char **buf, int n){ int i = 0; for (i = 0; i < n; i++) { free(buf[i]); buf[i] = NULL; } if (buf != NULL) { free(buf); buf = NULL; }}int main(void){ char **buf = NULL; int n = 3; buf = getMem(n); if (buf == NULL) { printf("getMem err\n"); return -1; } print_buf(buf, n); free_buf(buf, n); buf = NULL; printf("\n"); system("pause"); return 0;}1、一维数组的初始化 int a[] = { 1, 3, 5 }; //3个元素 int b[5] = { 1, 2, 3 }; //a[3], a[4]自动初始化为0 int c[10] = { 0 }; //全部元素初始化为0 memset(c, 0, sizeof(c)); //通过memset给数组每个元素赋值为0
    2、数组类型 int a[] = { 1, 3, 5 }; //3个元素 a: 数组首行首元素地址,一级指针 &a: 整个数组的首地址,二级指针
    首行首元素地址和首行(整个一维数组)地址值虽然是一样,但是它们的步长不一样a+1: 跳过1元素,一元素为4字节,步长4个字节&a+1: 跳过整个数组,整个数组长度为 3*4 = 12,步长 3 * 4 = 12sizeof(a): 传参为:数组首行首元素地址,测数组(int [3])的长度,3 * 4 = 12sizeof(a[0]): 传参为:数组首元素(不是地址),每个元素为int类, 4字节sizeof(&a):传参为:一维数组整个数组的地址(首行地址),编译器当做指针类型,4字节(重要)首行地址 --> 首行首元素地址(加*)&a:首行地址*&a -> a : 首行首元素地址//数组也是一种数据类型,类型本质:固定大小内存块别名//由元素类型和内存大小(元素个数)共同决定 int a[5] int[5]//可以通过typedef定义数组类型//有typedef是类型,没有typedef是变量typedef int ARRARY[5]; //定义了一个名字为ARRARY的数组类型//等价于typedef int (ARRARY)[5];//根据数组类型,定义变量//ARRARY的位置替代为d,去掉typedef,int d[5]ARRARY d; //相当于int d[5];3、指针数组(它是数组,每个元素都是指针) 1)指针数组的定义 //指针数组变量 //[]优先级比高,它是数组,每个元素都是指针(char ) char *str[] = { “111”, “2222222” };
    char **str = { "111", "2222222" }; //err2)指针数组做形参void fun(char *str[]);void fun(char **str); //str[] -> *str3)main函数的指针数组//argc: 传参数的个数(包含可执行程序)//argv:指针数组,指向输入的参数int main(int argc, char *argv[]);: demo.exe a b testint argc = 4char *argv[] = {"demo.exe", "a", "b", "test"}4、数组指针变量(它是指针变量,指向数组的指针) //定义数组变量 int a[10]; //有typedef:类型 //没有typedef:变量 1、根据数组类型,定义指针变量,数组指针变量 typedef int ARRARY[10]; //定义了一个名字为ARRARY的数组类型 //等价于typedef int (ARRARY)[10];
    ARRARY *p; //数组指针变量//编译会有警告,但不会出错,因为 a 和 &a的值一样//就算p = a这样赋值,编译器内部也会自动转换为 p = &a//不建议这么做p = a; //p 指向a数组,指向一维数组的指针p = &a; //如何操作数组指针变量 pint i = 0;for (i = 0; i < 10; i++){ (*p)[i] = i + 1; //p = &a //*p -> *(&a) -> a //(*p)[i] -> a[i]}2、直接定义数组指针变量(常用)//()[]同级,从左往右看//()有*,它是一个指针,[]代表数组//指向数组的指针变量,[]中的数字代表指针+1的步长int(*p)[10]; //p 指向a数组,指向一维数组的指针p = &a;3、先定义数组指针类型,再根据类型定义指针变量(常用)//和指针数组写法很类似,多了()//()和[]优先级一样,从左往右//()有指针,它是一个指针,[]//指向数组的指针,它有typedef,所有它是一个数组指针类型//数组指针类型,加上typedeftypedef int(*Q)[10];Q p; //根据类型定义变量,p是数组指针变量p = &a; //p指向数组a5、多维数组本质 1)二维数组初始化 int a1[3][4] = { {1, 2, 3, 4}, {5, 6, 7, 8}, {9, 10, 11, 12} }; int a2[3][4] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12 }; int a3[][4] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12 };
    2)内存中并不存在多维数组,多维数组在内存中都是线性存储int a[3][5] = { 0 };int *b = (int *)a;int i = 0;for(i = 0; i < 15; i++){ printf("%d ", b[i]);}printfA((int *)a,sizeof(a)/sizeof(a[0][0]));3)多维数组名//学会类比int b[5] = {0};b: 首行首元素地址, +1,跳 4 个字节&b:首行地址,+1,跳 4*5 = 20个字节//二维数组实际上就是 N 个一维数组//把二维数组第一个[]的值看做标志位,0 -> 2//第0个一维数组a[5] -> 第2个一维数组a[5]int a[3][5] = { 0 };a:二维数组首元素地址代表首行地址,相当于一维数组整个数组的地址,相当于上面的 &b,本来就是一个二级指针//(重要)首行地址 --> 首行首元素地址(加*)*a:首行首元素地址,相当于一维数组首元素地址,相当于上面的 ba + i -> &a[i]: 第i行地址//(重要)某行地址 --> 某行首元素地址(加*)*(a+i) -> *&a[i] -> a[i]: 第i行首元素地址//第i行j列元素的地址,某行首元素地址 + 偏移量*(a+i)+j -> a[i]+j -> &a[i][j]: 第i行j列元素的地址//第i行j列元素的值,第i行j列元素的地址的基础上(加 *)*(*(a+i)+j) -> a[i][j]: 第i行j列元素的值int a[3][5] = { 0 };sizeof(a): 二维数组整个数组长度,4 * 3 * 5 = 60sizeof(a[0]):a[0]为第0行首元素地址,相当于测第0行一维数组的长度:4 * 5 = 20sizeof(a[0][0]):a[0][0]为第0第0列元素(是元素,不是地址),测某个元素长度:4字节4)多维数组名,实际上是一个数组指针,指向数组的指针,步长为一行字节长度int a[3][5] = { 0 };//定义一个数组指针类型的变量int(*p)[5];//编译会有警告,但不会出错,因为 a 和 &a的值一样//但是&a代表整个二维数组的首地址//就算p = &a这样赋值,编译器内部也会自动转换为 p = a//不建议这么做p = &a;//a 本来就是第0个一维数组整个数组的地址,所以,无需加&p = a;5)二维数组做形参的三种形式//一维数组做函数参数退化为一级指针//二维数组(多维数组)做函数参数,退化为数组指针int a[3][5] = { 0 };void print_array1(int a[3][5]);//第一维的数组,可以不写//第二维必须写,代表步长,确定指针+1的步长 5*4void print_array2(int a[][5])//形参为数组指针变量,[]的数字代表步长void print_array3(int (*a)[5]);//a+1和二维数组的步长不一样//这里的步长为4//上面二维数组的步长为 5 * 4 = 20void print_array3(int **a); //err6、小结 typedef int A[10];//A:数组类型 A b;//int b[10],数组类型变量,普通变量 A *p;//数组类型定义数组指针变量
    typedef int (*P)[10];//数组指针类型P p;//数组指针变量int (*q)[10];//数组指针变量六 结构体1、结构体类型基本操作 1)结构体类型定义 //struct结构体关键字 //struct STU合起来才是类型名 //{}后面有个分号 struct Stu { char name[32]; char tile[32]; int age; char addr[50]; }; //通过typedef把struct Stu改名为Stu typedef struct Stu { int a; }Stu;
    2)结构体变量的定义//1)先定义类型,再定义变量,最常用struct Stu a;//全局变量、局部变量//2)定义类型的同时,定义变量struct _Stu{ char name[32]; char tile[32]; int age; char addr[50];}c;struct{ char name[32]; char tile[32]; int age; char addr[50];}e, f;3)结构体变量初始化//定义变量同时时初始化,通过{}struct Stu g = { "lily", "teacher", 22, "guangzhou" };4)变量和指针法操作结构体成员//变量法, 点运算符struct Stu h;strcpy(h.name, "^_^");(&h)->name//指针法, ->//结构体指针变量,没有指向空间,不能给其成员赋值struct Stu *p;p = &h;strcpy(p->name, "abc");(*p).name结构体也是一种数据类型,复合类型,自定义类型5)结构体数组
    //结构体类型 typedef struct Teacher { char name[32]; int age; }Teacher; //定义结构体数组,同时初始化 Teacher t1[2] = { { "lily", 18 }, { "lilei", 22 } }; //静态数组 Teacher t1[2] = {"lily", 18, "lilei", 22 }; int i = 0; for(i = 0; i < 2; i++) { printf(“%s, %d\n”, t1[i].name, t1[i].age);} //动态数组 Teacher *p = (Teacher *)malloc(3 * sizeof(Teacher)); if(p ==NULL) { return -1; } char buf[50]; int i; for(i = 0; i < 3; i++) { sprintf(buf,"name%d%d%d",i,i,i); strcpy(p[i].name,buf); p[i].age = 20 + i;}2、结构体赋值 //定义结构体类型是不要直接给成员赋值 //结构体只是一个类型,还没有分配空间 //只有根据其类型定义变量时,才分配空间,有空间后才能赋值 Teacher t1 = { “lily”, “teacher”, 18, “beijing” }; //相同类型的结构体变量,可以相互赋值 //把t1每个成员的内容逐一拷贝到t2对应的成员中 t1和t2没有关系 Teacher t2 = t1;
    3、结构体套指针
    1)结构体嵌套一级指针类型 typedef struct Teacher { char *name; int age; }Teacher; Teacher *p = NULL; p = (Teacher *)malloc(sizeof(Teacher)); p->name = (char *)malloc(30); strcpy(p->name,”lilei”); p->age = 22; 2)结构体嵌套二级指针类型 typedef struct Teacher { char *name; char **stu; int age; }Teacher; //1 Teacher t; //t.stu[3]; //char *t.stu[3];int n = 3; int i = 0; t.stu = (char **)malloc(n * sizeof(char *)); for(i = 0; i < n; i++) { t.stu[i] = (char *)malloc(30); strcpy(t.stu[i],”lily”); } //2 Teacher *p = NULL; //p->stu[3] p = (Teacher *)malloc(sizeof(Teacher)); //char *p->stu[3]p->stu = (char **)malloc(n * sizeof(char *));//3 Teacher *q = NULL; //Teacher *q[3] //q[i].stu[3] q = (Teacher *)malloc(sizeof(Teacher) * 3) for (i = 0; i < 3; i++) { //q[i].stu //(q+i)->stu q[i].stu = (char **)malloc(3 * sizeof(char *)); //char *stu[3] for(j = 0; j < 3; j++) { q[i].stu[j] = (char *)malloc(30); } }4、结构体做函数参数
    int getMem(Teacher **tmp, int n) { if(tmp == NULL) { return -1; } Teacher *p = (Teacher *)malloc(sizeof(Teacher) * 3); //Teacher q[3]; int i = 0; char buf[30]; for(i = 0; i < n; i++) { p[i].name = (char *)malloc(30); sprintf(buf,”name%d%d%d”, i, i, i); strycpy(p[i].name, buf); p[i].age = 20 + 2 * i;}*tmp = p;retrun 0; }5、浅拷贝和深拷贝 typedef struct Teacher { char name; int age; }Teacher; //结构体中嵌套指针,而且动态分配空间 //同类型结构体变量相互赋值 //不同结构体成员指针变量指向同一块内存 Teacher t1; t1.name = (char )malloc(30); strcpy(t1.name, “lily”); t1.age = 22;
    Teacher t2;t2 = t1;//深拷贝,人为增加内存,重新拷贝一下t2.name = (char *)malloc(30);strcpy(t2.name, t1.name);6、结构体偏移量(了解) //结构体类型定义下来,内部的成员变量的内存布局已经确定 typedef struct Teacher { char name[64]; //64 int age; //4 int id; //4 }Teacher;
    Teacher t1;Teacher *p = NULL;p = &t1;int n1 = (int)(&p->age) - (int)p; //相对于结构体首地址int n2 = (int)&((Teacher *)0)->age; //绝对0地址的偏移量7、结构体字节对齐(以空间换时间),详情请看《结构体字节对齐规则.doc》 原则1:数据成员的对齐规则(以最大的类型字节为单位)。原则2:结构体作为成员的对齐规则。 注意:

    结构体A所占的大小为该结构体成员内部最大元素的整数倍,不足补齐。不是直接将结构体A的成员直接移动到结构体B中 原则3:收尾工作结构体的总大小,也就是sizeof的结果,必须是其内部最大成员的整数倍,不足的要补齐。 struct A{ int a; double b; float c;};struct{ char e[2]; int f; int g; short h; struct A i;}B;//对齐单位 8 个字节sizeof(B) = 40//普通成员偏移量e: 2*0 = 0f: 4*1 = 4g: 4*2 = 8h: 2*6 = 12结构体起点坐标: 8*2 = 16//结构体成员偏移量a: 16 + 4*0 = 16b: 16 + 8*1 = 24c: 16 + 4*4 = 32
    七 文件7.1 基本概念7.1.1 文件分类 普通文件:存放在硬盘中的文件 设备文件:屏幕、键盘等特殊文件
    文本文件:ASCII文件,每个字节存放一个字符的ASCII码,打开文件看到的是文本信息二进制文件:数据按其在内存中的存储形式原样存放,打开文件看到的是乱码7.1.1文件缓冲区(了解) ANSI C(标准C语言库函数)标准采用“缓冲文件系统”处理数据文件。
    写文件(设备文件除外),并不会直接写到文件中,会先放在缓冲区,默认情况下,关闭文件或缓冲区满了才写到文件。如果没有关闭文件,缓冲区也没有满,可以通过程序正常结束,或者人为刷新缓冲区fflush(fd)来把缓冲区的内容写到文件中。缓冲区了解一下即可,增加缓冲区只是为了提高效率,减少频繁交互的次数,我们写程序基本上不用关心。7.2 读写文件步骤7.2.1 打开文件 //定义文件指针 FILE *fp = NULL; fopen(“c:\demo.txt”, “w+”); //“c:\demo.txt” windows有效 //“c:/demo.txt”: 文件路径,可以是绝对路径和相对路径 //“w+”: 打开权限,读写方式打开,文件不存在则创建,写内容时,会清空原来内容再写 //“r+”:读写方式打开,文件不存在则报错 fp = fopen(“./demo.txt”, “w+”); // 45度 “c:/demo.txt” linux windows都可用 if (fp == NULL) { perror(“fopen”); //打印错误信息 return; }
    默认情况下,VS, Qt相对路径说明:1)编译代码时,相对路径相对于工程目录2)直接点击可执行程序,相对路径相对于可执行程序//”C:\\Users” windows的写法//”C:/Users” Linux,windows都支持,建议”/”c语言中有三个特殊的文件指针无需定义、打开可直接使用:stdin: 标准输入 默认为当前终端(键盘)我们使用的scanf、getchar函数默认从此终端获得数据stdout:标准输出 默认为当前终端(屏幕)我们使用的printf、puts函数默认输出信息到此终端stderr:标准出错 默认为当前终端(屏幕)当我们程序出错或者使用: perror函数时信息打印在此终端fputc('a', stdout); //stdout -> 屏幕, 打印普通信息char ch;ch = fgetc(stdin); //std -> 键盘printf("ch = %c\n", ch);//fprintf(stderr, "%c", ch ); //stderr -> 屏幕, 错误信息fputc(ch, stderr);printf 标准输出sprintf 字符输出fprintf 文件输出7.2.2 读写文件 1、输出,即为写,把buf中的内容写到指定的文件中 2、输入,即为读,把文件中的内容取出放在指定的buf
    7.2.3 关闭文件 fclose(fp);
    if(fp != NULL){ fclose(fp); fp = NULL;}
    7.3 读写文件7.3.1 库函数的学习 1)包含所需头文件 2)函数名字 3)功能 4)参数 5)返回值
    7.3.2 按照字符读写文件:fgetc()、fputc() 1)写文件 char *str = “111abcdefg12345678”; int i = 0; for (i = 0; i < (int)strlen(str); i++) { //功能:往文件fp中写str[i],一个字符一个字符写 //参数:str[i]:写入文件的字符,fp:文件指针 //返回值:成功写入文件的字符,失败:-1 fputc(str[i], fp); }
    2)读文件char ch;//feof(fp)判断文件是否到结尾,已经到结尾返回值为非0,没有到结尾返回值为0while ( !feof(fp) ) //如果文件没有结尾{ //返回值:成功读取的字符 ch = fgetc(fp); printf("%c", ch);}7.3.4 按照行读写文件:fputs()、fgets() 1)写文件 char *buf[] = { “11111111\n”, “aaaaaaaaaaaa\n”, “bbbbbbbbbbbb\n” }; //指针数组 int i = 0; for (i = 0; i < 3; i++) { //功能:往文件fp写入一行内容buf[i] //参数:buf[i]:字符串首地址,fp:文件指针 //返回值:成功:0,失败:非0 fputs(buf[i], fp); }
    2)读文件char buf[512] = {0};//从文件中读取一行内容(以"\n"作为标志),放在buf中//一次最大只能读sizeof(buf)-1,如果小于sizeof(buf)-1,则按实际大小读取//然后在字符串结尾自动加上字符‘\0’(转换为C风格字符串)//返回值:成功:读出的字符串,失败:NULLif (fgets(buf, sizeof(buf), fp) != NULL) { printf("buf = %s", buf);}7.3.5 按照块读写文件:fread()、fwirte() typedef struct Stu { char name[50]; int id; }Stu; Stu s[3];
    1)写文件//写文件,按块的方式写//s:写入文件内容的内存首地址//sizeof(Stu):块数据的大小//3:块数, 写文件数据的大小 sizeof(Stu) *3//fp:文件指针//返回值,成功写入文件的块数目,不是数据总长度int ret = fwrite(s, sizeof(Stu), 3, fp);printf("ret = %d\n", ret);2)读文件//读文件,按块的方式读//s:放文件内容的首地址//sizeof(Stu):块数据的大小//3:块数, 读文件数据的大小 sizeof(Stu) *3//fp:文件指针//返回值,成功读取文件内容的块数目,不是数据总长度int ret = fread(s, sizeof(Stu), 3, fp);printf("ret = %d\n", ret);7.3.6 按照格式化进行读写文件:fprintf()、fscanf() 1)写文件 //格式化写文件 int a = 250; int b = 10; int c = 20; //和printf()用法一样,只是printf是往屏幕(标准输出)写内容 //fprintf往指定的文件指针写内容 //返回值:成功:写入文件内容的长度,失败:负数 fprintf(fp, “Tom = %d, just like %d, it is %d”, a, b, c);
    2)读文件int a, b, c;fscanf(fp, "Tom = %d, just like %d, it is %d", &a, &b, &c);printf("a = %d, b = %d, c = %d\n", a, b, c);7.3.7 随机读写 //文件光标移动到文件结尾 //SEEK_SET:文件开头 //SEEK_CUR:文件当前位置 //SEEK_END:文件结尾 fseek(fp, 0, SEEK_END);
    //获取光标到文件开头文件的大小ftelllong size = ftell(fp);//文件光标恢复到开始位置rewind(fp);typedef struct Stu{ char name[50]; int id;}Stu;Stu tmp; //读第3个结构体//假如文件中写了三个结构体//从起点位置开始,往后跳转2个结构体的位置fseek(fp, 2*sizeof(Stu), SEEK_SET);//从结尾位置开始,往前跳转一个结构体的位置//fseek(fp, -1 * (int)sizeof(Stu), SEEK_END);int ret = 0;ret = fread(&tmp,sizeof(Stu), 1, fp);if(ret == 1){ printf("[tmp]%s, %d\n", tmp.name, tmp.id);}//把文件光标移动到文件开头//fseek(fp, 0, SEEK_SET);rewind(fp);7.4 综合案例1)加密文件读写(使用别人写好的接口) 加密 解密2)配置文件读写(自定义接口)
    八 链表1、数组和链表的区别 数组:一次性分配一块连续的存储区域 优点: 随机访问元素效率高 缺点: 需要分配一块连续的存储区域(很大区域,有可能分配失败) 删除和插入某个元素效率低
    链表:现实生活中的灯笼 优点: 不需要一块连续的存储区域 删除和插入某个元素效率高 缺点: 随机访问元素效率低2、相关概念 节点:链表的每个节点实际上是一个结构体变量,节点,既有 数据域 也有 指针域 typedef struct Node { int id; //数据域 struct Node *next; //指针域 }SLIST;
    尾结点:next指针指向NULL3、结构体套结构体 typedef struct A { int b; }A; / 1)结构体可以嵌套另外一个结构体的任何类型变量 2)结构体嵌套本结构体普通变量(不可以) 本结构体的类型大小无法确定,类型本质:固定大小内存块别名 3)结构体嵌套本结构体指针变量(可以) 链表 指针变量的空间能确定,32位, 4字节, 64位, 8字节
    / typedef struct B { int a; A tmp1; //ok A p1; //ok //struct B tmp2; err struct B next; //32位, 4字节, 64位, 8字节 }B;
    4、链表的分类 1)单向带头链表和不带头链表
    2)双向链表带头链表和不带头链表
    3)双向循环链表带头链表和不带头链表
    5、链表的使用实际上是指针的拓展应用:指向指向谁,就把谁的地址赋值给指针。 typedef struct Stu { int id; //数据域 char name[100]; struct Stu *next; //指针域 }Stu;
    (1)静态链表//初始化三个结构体变量Stu s1 = { 1, "mike", NULL };Stu s2 = { 2, "lily", NULL };Stu s3 = { 3, "lilei", NULL };s1.next = &s2; //s1的next指针指向s2s2.next = &s3;s3.next = NULL; //尾结点Stu *p = &s1; while (p != NULL){ printf("id = %d, name = %s\n", p->id, p->name); //结点往后移动一位 p = p->next; //&s2}(2)动态链表//Stu *p1 = NULL;//p1 = (Stu *)malloc(sizeof(Stu));Stu *p1 = (Stu *)malloc(sizeof(Stu));Stu *p2 = (Stu *)malloc(sizeof(Stu));Stu *p3 = (Stu *)malloc(sizeof(Stu));p1->next = p2;p2->next = p3;p3->next = NULL; //尾节点Stu *tmp = p1;while(tmp != NULL){ printf("id = %d, name = %s\n", tmp->id, tmp->name); //结点往后移动一位 tmp = tmp->next;}6、链表的增、删、改、查操作
    1)单向链表基本操作#define _CRT_SECURE_NO_WARNINGS#include <stdio.h>#include <stdlib.h>#include <string.h>typedef struct Node{ int id; //数据域 struct Node *next; //指针域}Node;//创建头结点//链表的头结点地址由函数值返回。Node *SListCreat(){ Node *head = NULL; //头结点作为标志,不存储有效数据 head = (Node *)malloc(sizeof(Node)); if (head == NULL) { return NULL; } //给head的成员变量赋值 head->id = -1; head->next = NULL; Node *pCur = head; Node *pNew = NULL; int data; while (1) { printf("请输入数据:"); scanf("%d", &data); if (data == -1) //输入-1,退出 { break; } //新结点动态分配空间 pNew = (Node *)malloc(sizeof(Node)); if (pNew == NULL) { //continue; break; } //给pNew成员变量赋值 pNew->id = data; pNew->next = NULL; //链表建立关系 //当前结点的next指向pNew pCur->next = pNew; //pNew下一个结点指向NULL pNew->next = NULL; //把pCur移动到pNew,pCur指向pNew pCur = pNew; } return head;}//链表的遍历int SListPrint(Node * head){ if (head == NULL) { return -1; } //取出第一个有效结点(头结点的next) Node *pCur = head->next; printf("head -> "); while (pCur != NULL) { printf("%d -> ", pCur->id); //当前结点往下移动一位,pCur指向下一个 pCur = pCur->next; } printf("NULL\n"); return 0;}//在值为x的结点前,插入值为y的结点;若值为x的结点不存在,则插在表尾。int SListNodeInsert(Node * head, int x, int y){ if (head == NULL) { return -1; } Node *pPre = head; Node *pCur = head->next; while (pCur != NULL) { if (pCur->id == x) //找到了匹配结点 { break; } //pPre指向pCur位置 pPre = pCur; pCur = pCur->next; //pCur指向下一个结点 } //2种情况 //1. 找匹配的结点,pCur为匹配结点,pPre为pCur上一个结点 //2. 没有找到匹配结点,pCur为空结点,pPre为最后一个结点 //给新结点动态分配空间 Node *pNew = (Node *)malloc(sizeof(Node)); if (pNew == NULL) { return -2; } //给pNew的成员变量赋值 pNew->id = y; pNew->next = NULL; //插入指定位置 pPre->next = pNew; //pPre下一个指向pNew pNew->next = pCur; //pNew下一个指向pCur return 0;}//删除第一个值为x的结点int SListNodeDel(Node *head, int x){ if (head == NULL) { return -1; } Node *pPre = head; Node *pCur = head->next; int flag = 0; //0没有找,1找到 while (pCur != NULL) { if (pCur->id == x) //找到了匹配结点 { //pPre的下一个指向pCur的下一个 pPre->next = pCur->next; free(pCur); pCur = NULL; flag = 1; break; } //pPre指向pCur位置 pPre = pCur; pCur = pCur->next; //pCur指向下一个结点 } if (0 == flag) { printf("没有值为%d的结点\n", x); return -2; } return 0;}//清空链表,释放所有结点int SListNodeDestroy(Node *head){ if (head == NULL) { return -1; } Node * tmp = NULL; int i = 0; while (head != NULL) { //保存head的下一个结点 tmp = head->next; free(head); head = NULL; //head指向tmp head = tmp; i++; } printf("i = %d \n", i); return 0;}//删除值为x的所有结点int SListNodeDelPro(Node *head, int x){ if (head == NULL) { return -1; } Node *pPre = head; Node *pCur = head->next; int flag = 0; //0没有找,1找到 while (pCur != NULL) { if (pCur->id == x) //找到了匹配结点 { //pPre的下一个指向pCur的下一个 pPre->next = pCur->next; free(pCur); pCur = NULL; flag = 1; pCur = pPre->next; //break; continue; //跳出本次循环,重要 } //pPre指向pCur位置 pPre = pCur; pCur = pCur->next; //pCur指向下一个结点 } if (0 == flag) { printf("没有值为%d的结点\n", x); return -2; } return 0;}//链表节点排序int SListNodeSort(Node *head){ if(head == NULL || head->next == NULL) { return -1; } Node *pPre = NULL; Node *pCur = NULL; Node tmp; // pPre->next != NULL,链表倒数第2个结点 for (pPre = head->next; pPre->next != NULL; pPre = pPre->next) { for (pCur = pPre->next; pCur != NULL; pCur = pCur->next) { //注意,排序,除了数据域需要交换,next指针还需要交换 if (pPre->id > pCur->id) //升序 { //只交换数据域 tmp.id = pCur->id; pCur->id = pPre->id; pPre->id = tmp.id; } } } return 0;}//假如原来链表是升序的,升序插入新节点//不能插入节点后再排序,是升序插入新节点xint SListNodeInsertPro(Node *head, int x){ //保证插入前是有序的 int ret = SListNodeSort(head); if (ret != 0) { return ret; } if (head == NULL) { return -1; } Node *pPre = head; Node *pCur = head->next; //1 2 3 5 6, 插入4 //3:pre, 5: cur while (pCur != NULL) { if (pCur->id > x) //找到了匹配结点 { break; } //pPre指向pCur位置 pPre = pCur; pCur = pCur->next; //pCur指向下一个结点 } //给新结点动态分配空间 Node *pNew = (Node *)malloc(sizeof(Node)); if (pNew == NULL) { return -2; } //给pNew的成员变量赋值 pNew->id = x; pNew->next = NULL; //插入指定位置 pPre->next = pNew; //pPre下一个指向pNew pNew->next = pCur; //pNew下一个指向pCur return 0; return 0;}//翻转链表的节点(不是排序,是翻转)//把链表的指向反过来int SListNodeReverse(Node *head){ if (head == NULL || head->next == NULL || head->next->next == NULL) { return -1; } Node *pPre = head->next; Node *pCur = pPre->next; pPre->next = NULL; // head->next->next = NULL; Node *tmp = NULL; while (pCur != NULL) { tmp = pCur->next; pCur->next = pPre; pPre = pCur; pCur = tmp; } //head->next->next = NULL; head->next = pPre; return 0;}int main(void){ Node *head = NULL; head = SListCreat();//创建头结点 SListPrint(head); SListNodeInsert(head, 5, 4); printf("在5的前面插入4后\n"); SListPrint(head); SListNodeDelPro(head, 5); printf("删除所有5结点后\n"); SListPrint(head); SListNodeSort(head); printf("排序后\n"); SListPrint(head); SListNodeInsertPro(head, 6); printf("升序插入6后\n"); SListPrint(head); SListNodeReverse(head); printf("链表翻转后\n"); SListPrint(head); SListNodeDestroy(head); head = NULL; printf("\n"); system("pause"); return 0;}九 函数指针1、指针函数,它是函数,返回指针类型的函数 //指针函数 //()优先级比高,它是函数,返回值是指针类型的函数 //返回指针类型的函数 int fun() { int p = (int )malloc(sizeof(int)); return p; }
    2、函数指针,它是指针,指向函数的指针,(对比数组指针的用法) 一个函数在编译时被分配一个入口地址,这个地址就称为函数的指针,函数名代表函数的入口地址。
    函数指针变量,它也是变量,和int a变量的本质是一样的。int fun(int a){ printf("a ========== %d\n", a); return 0;}//定义函数指针变量有3种方式:(1)先定义函数类型,根据类型定义指针变量(不常用)//有typedef是类型,没有是变量typedef int FUN(int a); //FUN是函数类型,类型模式为: int fun(int);FUN *p1 = NULL; //函数指针变量p1 = fun; //p1 指向 fun 函数fun(5); //传统调用p1(6); //函数指针变量调用方式(2)先定义函数指针类型,根据类型定义指针变量(常用)//()()优先级相同,从左往右看//第一个()代表指针,所以,它是指针//第二个括号代表函数,指向函数的指针typedef int(*PFUN)(int a); //PFUN是函数指针类型PFUN p2 = fun; //p2 指向 funp2(7);(3)直接定义函数指针变量(常用)int(*p3)(int a) = fun;p3(8);int(*p4)(int a);p4 = fun;p4(9);3、函数指针数组,它是数组,每个元素都是函数指针类型 void add() {} void minus() {} void multi() {} void divide() {} void myexit() {}
    //函数指针变量,fun1指向add()函数void(*fun1)() = add;fun1(); //调用add()函数//函数指针数组void(*fun[5])() = { add, minus, multi, divide, myexit };//指针数组char *buf[] = { "add", "min", "mul", "div", "exit" };char cmd[100];int i = 0;while (1){ printf("请输入指令:"); scanf("%s", cmd); for (i = 0; i < 5; i++) { if (strcmp(cmd, buf[i]) == 0) { fun[i](); break; //跳出for()循环,最近的循环 } }}4、回调函数,函数的形参为:函数指针变量 int add(int a, int b) { return a + b; }
    int minus(int a, int b){ return a - b;}//int(*p)(int a, int b), p 为函数指针变量//框架,固定变量//多态,多种形式,调用同一种接口,不一样表现void fun(int x, int y, int(*p)(int a, int b) ){ int a = p(x, y); //回调函数 printf("a = %d\n", a);}typedef int(*Q)(int a, int b); //Q 为函数指针类型void fun2(int x, int y, Q p)//p 为函数指针变量{ int a = p(x, y); //回调函数 printf("a = %d\n", a);}//fun()函数的调用方式fun(1, 2, add);fun2(10, 5, minus);5、函数的递归 递归:函数可以调用函数本身(不要用main()调用main(),不是不行,是没有这么做,往往得不到你想要的结果)
    (1)普通函数调用(栈结构,先进后出,先调用,后结束)void funB(int b){ printf("b = %d\n", b); return;}void funA(int a){ funB(a-1); printf("a = %d\n", a);}调用流程:funA(2) -> funB(1) -> printf(b) (离开funB(),回到funA()函数)-> printf(a)(2)函数递归调用(调用流程和上面是一样,换种模式,都是函数的调用而已)void fun(int a){ if(a == 1) { printf("a == %d\n", a); return; //中断函数很重要 } fun(a-1); printf("a = %d\n", a);}fun(2);(3)递归实现累加 1+2+3+……+100#define _CRT_SECURE_NO_WARNINGS#include <stdio.h>#include <stdlib.h>#include <string.h>int fun(int n){ if (n == 1) { return n; } else { return fun(n - 1) + n; }}int main(void){ int i = 0; int sum = 0; for (i = 1; i <= 100; i++) { sum += i; } printf("sum1 = %d\n", sum); sum = fun(100); printf("\nsum2 = %d\n", sum); printf("\n"); system("pause"); return 0;}(4)函数递归字符串反转十 预处理1、C编译器提供的预处理功能主要有以下四种: 1)文件包含 #include 2)宏定义 #define 3)条件编译 #if #endif .. 4)一些特殊作用的预定义宏
    2、#include< > 与 #include “”的区别“”表示系统先在file1.c所在的当前目录找file1.h,如果找不到,再按系统指定的目录检索。< >表示系统直接按系统指定的目录检索。注意:
    1. #include <>常用于包含库函数的头文件2. #include ""常用于包含自定义的头文件3. 理论上#include可以包含任意格式的文件(.c .h等) ,但我们一般用于头文件的包含。3、宏定义
    #define 宏名 字符串#define PI 3.14#define TEST(a,b) (a)*(b)宏的作用域取消宏定义#undef 宏名4、宏定义函数
    #define MAX2(a,b) (a) > (b) ? (a) : (b)#define MAX3(a,b,c) (a) > (MAX2(b,c)) ? (a) : (MAX2(b,c))5、条件编译 防止头文件被重复包含引用//#pragma once
    //_FUN_H_ 自定义宏,每个头文件的宏都不一样//假如test.h->_TEST_H_#ifndef _FUN_H_#define _FUN_H_ //函数的声明 //宏定义 //结构体#endif //!_FUN_H_6、动态库的封装和使用 socketclient
    7、日志打印 FILE LINE
    8、内存泄漏检查 memwatch
    0 留言 2019-07-30 16:00:39 奖励11点积分
  • 【Cocos Creator实战教程(4)】——炸弹人(TiledMap相关) 精华

    1. 相关知识点
    虚拟手柄(新)
    地图制作
    碰撞检测
    动画制作

    2. 步骤2.1 制作地图2.1.1.新建19x19的地图,Tile大小32x32,导入图块资源2.1.2 建立三个图层(ground,hide,main)和一个对象层(objects)ground是背景层,用绿色的草坪图块填充满

    hide是道具层,挑选道具图块(包括出口的门)放在几个位置

    main是障碍物层,还原经典炸弹人的地图布局,每隔一个位置放一个钢块,用土块覆盖道具层的道具,在其他位置再放几个土块

    objects层有两个对象(player,enemy)标记玩家和敌人的位置信息
    最终效果如图

    2.1.3 将除了草坪图块的其他图块添加type属性,属性值分别为,soil(土块),steel(钢块),door(门),prop1(道具1)…
    2.1.4 保存地图文件和图集文件放在一起2.2 绑定地图2.2.1 新建工程Bomberman,将所需资源全部导入,项目结构如下(有些是后来添加的)
    2.2.2 将地图拖入场景(自动生成map和layer节点),将锚点改为(0,0),下面出现的player等节点也都是(0,0)为锚点2.2.3 新建脚本game.js添加为map节点组件,声明全局变量,在属性面板中拖入相关层节点
    2.3 虚拟手柄2.3.1 我们准备了上下左右炸弹五个按钮的原始状态和按下状态共十张图片素材,在场景编辑器中看着顺眼的地方摆放这五个按钮(将label删除),并将按钮的相应状态的按钮图片添加到右侧属性面板中2.3.2 为每个按钮添加点击事件,(map->game->btnBomb, btnUp, btnDown, btnLeft, btnRight)
    2.4 炸弹动画2.4.1 将炸弹图片集中的任意一个拖入场景中,将其宽高改为32x32,再拖回资源管理器中制作一个炸弹的prefab bomb,为其添加Animation组件2.4.2 新建炸弹爆炸动画clip explode,拖入bomb的属性面板2.4.3 在动画播放完成的最后一帧加入一个帧事件,响应事件方法名为exploded
    2.5 主角和敌人2.5.1 添加player和enemy为map节点的两个子节点,锚点0,0,大小32x32 (位置随意,一会我们要在代码中动态设置他们的位置)
    3. 总结
    虚拟手柄是一个比较热点的控制,现实生活中还会有圆圈控制各个角度的,也有开源项目大家可以参考
    注意节点前后层的遮挡关系

    注意:本教程部分素材来源于网络,另请大家在评论区踊跃提问发言。
    6 留言 2018-11-24 23:12:26 奖励20点积分
  • C语言学习笔记

    1 愉快的开始hello world1.1 include头文件包含include是要告诉编译器,包含一个头文件;
    在C语言当中,任何库函数调用都需要提前包含头文件;

    <头文件>,代表让C语言编译器去系统目录下寻找相关的头文件;
    “头文件”, 代表让C语言编译器去用户当前目录下寻找相关的头文件;

    如果是使用了一个C语言库函数需要的头文件,那么一定是#include < >;
    如果是使用了一个自定义的h文件,那么一定是#include“ ”。
    1.2 main函数main函数是C语言中的主函数,一个C语言的程序必须有一个主函数,也只能有一个主函数。
    int main() //一个函数在C语言里面,如果只是空(),代表这个函数可以有参数,也可以没有参数int main(void) //如果是(void),就是明确的表达没有任何参数1.3 注释
    // 单行注释,代表注释,就是一个文字说明,没有实质的意义,单行注释是C++语言的注释方法;
    / / 多行注释,多行注释是标准C语言的注释方法。

    1.4 {}括号,程序题和代码块C语言所有的函数的代码都是在{}里包着的
    1.5 声明int a;
    声明一个变量名字叫a,对于C语言,变量的名称是可以自定义的
    1.6 C语言自定义名字的要求可以使用大小写字母,下划线,数字,但第一个字母必须是字母或者下划线(Linux命名法、匈牙利命名法)

    字母区分大小写
    不能用C语言的关键字作为变量名称
    每一行,必须是;结尾

    1.7 printf函数printf是向标准输出设备输出字符串的如果要输出一个字符串:例如:printf(“hello world”);如果要输出一个整数,例如:printf(“%d”,整数);Printf(“\n”);会输出一个回车换行
    1.8 return语句一个函数遇到return语句就终止了,return是C语言的关键字
    1.9 System系统调用System库函数的功能是执行操作系统的命令或者运行指定的程序
    2 C语言中的数据类型2.1 常量常量就是在程序中不可变化的量,常量在定义的时候必须给一个初值。
    2.1.1 #define#define MAX 10 //定义一个宏常量2.1.2 constConst int a=20; //定义一个const常量
    2.2 字符串常量#define STRING “hello world\n”对于#define类型的常量,C语言的习惯是常量名称为大写,但对于普通const常量以及变量,一般为小写结合大写的方式
    2.3 二进制数、位、字节与字我们习惯于十进制的数:10,12等

    一个位只能表示0,或者1两种状态,简称bit
    一个字节为8个二进制,称为8位,简称BYTE,8个比特是一个字节
    一个字位2个字节,简称WORD
    两个字为双字,简称DWORD

    2.4 八进制八进制为以8为基数的数制系统,C语言当中表示八进制0
    2.5 十六进制
    十六进制值16为基数的数制系统,C语言当中用0x表示十六进制
    十进制转化为八进制,用十进制数作为被除数,8作为除数,取商数和余数,直到商数为0的时候,将余数倒过来就是转化后的结果
    十进制转化为十六进制,用十进制数作为被除数,16作为除数,取商数和余数,直到商数为0的时候,将余数倒过来就是转化后的结果

    2.6 原码将最高位作为符号位(0代表正,1代表负),其余各位代表数值本身的绝对值
    2.7 反码
    一个数如果值为正,那么反码和原码相同
    一个数如果为负,那么符号位为1,其他各位与原码相反

    2.8 补码
    正数:原码、反码和补码都相同
    负数:最高位为1,其余各位原码取反,最后对整个数+1
    补码符号位不动,其他位求反,最后整个数+1,得到原码
    用补码进行运算,减法可以通过加法实现

    2.9 sizeof关键字
    Sizeof是C语言关键字,功能是求指定数据类型在内存中的大小,单位:字节,sizeof(int);
    Sizeof与size_f类型

    2.10 int类型2.10.1 int常量,变量int就是32位的一个二进制整数,在内存当中占据4个字节的空间
    2.10.2 printf输出int值
    %d,输出一个有符号的10进制整数
    %u,代表输出一个无符号的10进制整数

    2.10.3 printf输出八进制和十六进制
    %o,代表输出八进制整数
    %x,代表输出16进制整数
    %X,用大写字母方式输出16进制数

    2.10.4 short,long,long long,unsigned int
    Short意思为短整数,在32位系统下是2个字节,16个比特
    Long 意思为长整数,在32位系统下,long都是4个字节的,在64位系统下,windows还是4个字节,unix下成了8个字节。
    int不管是32位系统下,还是64位系统下,不论是windows还是unix都是4个字节的。
    Long long是64位,也就是8个字节大小的整数,对于32位操作系统,CPU寄存器是32位,所以计算long long类型的数据,效率很低

    2.10.5 整数溢出计算一个整数的时候超过整数能够容纳的最大单位后,整数会溢出,溢出的结果是高位舍弃。当一个小的整数赋值给大的整数,符号位不会丢失,会继承。
    2.10.6 大端对齐小端对齐对于ARM,intel这种x86构架的复杂指令CPU,整数在内存中是倒着存放的,低地址放地位,高地址放高位,小端对齐;
    但对于unix服务器的CPU,更多是采用大端对齐的方式存放整数。
    2.11 char类型2.11.1 char常量,变量
    Char c; //定义一个char变量
    ‘a’,char的常量
    char的本质就是一个整数,一个只有1个字节大小的整数

    2.11.2 printf输出char%c意思是输出一个字符,而不是一个整数
    2.11.3 不可打印char转义符\a 警报\b 退格\n 换行\r 回车\t 制表符\\ 斜杠\’ 单引号\” 双引号\?问号2.11.4 char和unsigned char
    Char取值范围为-128到127
    Unsigned char为0-255

    2.12 浮点float,double,long double类型2.12.1 浮点常量,变量
    Float在32位系统下是4个字节,double在32位系统下是8个字节
    小数的效率很低,避免使用,除非明确的要计算一个小数。

    2.12.2 printf输出浮点数
    %f是输出一个double
    %lf输出一个long double

    2.13 类型限定2.13.1 constConst是代表一个不能改变值的常量
    2.13.2 volatile代表变量是一个可能被CPU指令之外的地方改变的,编译器就不会针对这个变量去优化目标代码。
    2.13.3 register变量在CPU寄存器里面,而不是在内存里面,但register是建议型的指令,而不是命令型的指令。
    3 字符串格式化输出和输入3.1 字符串在计算机内部的存储方式字符串是内存中一段连续的char空间,以‘\0’结尾
    “”是C语言表达字符串的方式
    3.2 printf函数,putchar函数Printf格式字符
    字符 对应数据类型 含义d int 接受整数值并将它表示为有符号的十进制整数hd short int 短整数hu unsigned shor int 无符号短整数o unsigned int 无符号8进制整数u unsigned int 无符号10进制整数x/X unsigned int 无符号16进制整数,x对应的是abcdf,X对应的是ABCDEFf float或double 单精度浮点数或双精度浮点数e/E double 科学计数法表示的数,此处“e”的大小写代表在输出是用“e”的大小写c char 字符型,可以把输入的数字按照ASC11码相应转换为对应的字符s/S char */wchar_t * 字符串,输出字符串中的字符直至字符串的空字符(字符串以‘\0’结尾,这个‘\0’即空字符)p void 以16进制形式输出指针% % 输出一个百分号Printf附加格式
    字符 含义| 附加在d,u,x,o前面,表示长整数- 左对齐m(代表一个整数) 数据最小宽度0 将输出的前面补上0,直到占满指定列宽为止(不可以搭 配使用“-”)N(代表一个整数) 宽度至少为n位,不够以空格填充。Putchar是显式一个字符的函数
    long l = 100;Printf(“-6ld”,l);3.3 scanf函数与getchar函数Scanf通过键盘读取用户输入,放入变量中,记得参数一定是变量的地址(&)
    // #define _CRT_SECURE_NO_WARNINGS#pragma warning(disable:4996)Int a=0;Scanf(“%d”,&a);//一定要用&取变量的地址getchar得到用户键盘输入的字符char a = 0;a = getchar();//得到用户键盘的按键printf(“%c”,a);printf(“please input a:”);scanf(“%d”,&a);getchar(); //通过getchar这个函数将之前输入a时候用户按的回车键先收到4 运算符表达式和语句4.1 基本运算符4.1.1 =
    数据对象:泛指数据在内存的存储区域
    左值:表示可以被更改的数据对象
    右值:能赋给左值的量

    4.1.2 +加
    4.1.3 –减
    4.1.4 *乘
    4.1.5 /除
    4.1.6 %取余数
    4.1.7 + =加等于

    a+=5;
    4.1.8 - =减等于
    4.1.9 * =乘等于
    4.1.10 /=除等于
    4.1.11 %=取余等于
    4.1.12 ++自加1

    i++先计算表达式的值,然后再++
    ++i是先++,再计算表达式的值

    4.1.13 —自减1
    4.1.14 逗号运算符int a = 2;int b = 3;int c = 4;int d = 5;int I = (a = b, c + d);逗号表达式先求逗号左边的值,然后求右边的值,整个语句的值是逗号右边的值。
    4.1.15 运算符优先级优先级 运算符 结合性 1 ++(后缀),--(后缀),()(调试函数), 从左到右 {}(语句块),-> 2 ++(前缀),--(前缀),+(前缀),-(前缀), 从右到左 ! (前缀),~(前缀),sizeof,*(取指针值), &(取地址),(type)(类型转化) 3 *,/,% 从左到右 4 +,- 从左到右 5 <<,>> 从左到右 6 < ,>,<=,>= 从左到右 7 = =,!= 从左到右 8 & 从左到右 9 ^ 从左到右 10 | 从左到右 11 && 从左到右 12 || 从左到右 13 ? 从右到左 14 =,*=,%=,+=,-=,<<=,>>=,&=, 从右到左 |=,^= 15 ,(逗号运算符) 从左到右4.2 复合语句{}代码块
    for(i = 0; i < 3; i++) //循环语句,代表复合语句内部的代码要执行3次{ printf(“hello\n”);}4.3 空语句只有一个;号的语句就是空语句,空语句在C语言里面是合法的,并且是在某些场合必用的
    4.4 类型转化double f = (double) 3 / 2; //(double)3意思是将整数3强制转化为double型 ()为强制类型转化运算符double f = 3 /2; //C语言两个整数相除的结果自动转化为一个整数double f = 3.0 / 2;5 条件分支语句5.1 关系运算符在C语言中0代表false,非0代表真
    5.1.1 <小于
    5.1.2 <=小于等于
    5.1.3 >大于
    5.1.4 >=大于等于
    5.1.5 = =等于
    5.1.6 !=不等于
    5.2 关系运算符优先级前四种相同,后两种相同,前四种高于后两种优先级
    5.3 逻辑运算符5.3.1 &&与
    当运算符左右都是真的时候,那么整个表达式的结果为真;只有左右有一个值为假,那么整个表达式的结果为假。
    5.3.2 | |或
    当运算符左右只要有一个值是真的时候,那么整个表达式的结果为真;除非左右两个值都是假,那么整个表达式的结果为假。
    5.3.3 !非
    当值为真的时候,表达式为假;当值为假的时候,表达式为真。
    5.4 if单分支
    if(条件){ //复合语句}当条件是真的时候,复合语句才能被执行,如果条件为假的时候,复合语句不执行
    5.5 if else双分支
    if(条件){ 复合语句1;}else{ 复合语句2;}如果条件为真,那么执行复合语句1,否则执行复合语句2
    5.6 if else if多重if
    if(条件1){ 复合语句1;}else if(条件2){ 复合语句2;}else if(条件3){ 复合语句3;}else { 复合语句4;}当有多个else的时候,else总是和上方最近的那个if语句配对
    5.7 switch与break,default多重选择
    switch(i){case 0: break; //跳出switch的复合语句块……default: //如果所有条件都不满足,那么执行default语句}什么时候用if,什么时候用switch?当条件很复杂,一个条件有&&,||,!存在,那么用if语句如果条件很简单,但分支很多,那么适合用switch
    5.8 条件运算符?一个求绝对值的例子
    int i = -8;int x = (i < 0) ? –i : i;先求?左边的条件,如果条件为真,那么等于:左边的值,否则等于:右边的值一个求最大值的例子
    int c = ( a > b ) ? a : b ;5.9 goto语句与标号无条件跳转goto
    goto end; //无条件的跳转到一个标号去执行……end://标号不建议使用goto语句,goto语句会使你的程序可读性变得很差
    6 循环语句6.1 whilewhile(条件),如果条件为真,循环继续,条件为假,循环结束
    while(1) //是死循环的写法{ 复合语句;}6.2 continue循环遇到continue语句,不再执行continue下面代码,而是直接返回到循环起始语句处继续执行循环
    6.3 break循环遇到break语句,立刻中断循环,循环结束
    6.4 do whiledo{ 复合语句;}while(条件);对于do while来讲,循环的复合语句至少可以被执行一次对于while来讲,有可能复合语句一次执行的机会都没有
    6.5 for
    先执行i=0,对于一个for循环,第一步只执行一次;
    判断i是否小于10,如果i小于10,那么循环继续,否则循环中断
    i++,第一次执行for的时候,不执行i++

    for(int i = 0 ; i < 10 ; i++){ 复合语句;}等同于:
    int = 0;while(i < 10){ i++;}6.6 循环嵌套打印三角形
    * *** ***** ****************int main(){ int i,j; for(i=1;i<7;i++) { for(j=1;j<7-i;j++) { printf(“ ”);}for(j=0;j<(i*2-1);j++){ printf(“*”);}printf(“\n”);}return 0;}7 数组数组的本质就是可以一次定义多个类型相同的变量,同时一个数组中所有的元素在内存中都是顺序存放的。但要记得在C语言中如果定义了如下数组:
    Char s[100] ;//s[0] – s[99],切记没有s[100]这个元素,而且C语言编译器不会帮你检查数组的下标是否有效。Char array[2][3][4] = {};//原则,数组维数越多,代码的可读性就越差,所以要尽可能的用维数少的数组7.1 一维数组定义与使用int array [10]; //定义一个一维数组,名字叫array,一共有10个元素,每个元素都是int类型的array[0] = 20 ;array[1] = 30 ;array[9] = 90 ;//array[10] = 100 ; //错误,没有array[10]这个元素7.2 数组在内存的存储的方式数组在内存中就是一段连续的空间,每个元素的类型是一样的
    7.3 一维数组初始化int array[10] = {1,2,3,4,5,6,7,8,9,10} ;//定义数组的同时为数组的成员初始化值int array[10] = {3,4,5} ;//将数组的前三个元素赋值,其余元素置为0int array[10] = {0} ;//将数组所有的元素都置为0int i;for (i = 0; i < 10; i++){ array[i] = 0 ;//通过循环遍历数组的每个元素,将元素的值置为0 //scanf(“%d”,&array[i]);}求数组中最大元素的值
    int main(){ int array[10] = {32,5,67,98,12,54,8,78,457,10};int max = 0;int i;for (i = 0; i < 10; i++) //想找最大的值,一定要把数组先遍历一遍{ if(max < array[i]) max = array[i]; } printf(“max = %d\n”,max); return 0;}求数组中最小元素的值,和最小值对应的数组下标
    int main(){ int array[10] = {32,5,67,98,12,54,8,78,457,10};int min = array[0];int index = 0; //在没有遍历数组之前,默认数组的第0号元素就是最小的元素int i;for (i = 1; i < 10; i++) //想找最小的值,一定要把数组先遍历一遍{ if(min > array[i]) { index = i; min = array[i]; } } printf(“min = %d index = %d\n”, min , index); return 0;}求数组中所有元素的和
    int main(){ int array[10] = {1,2,3,4,5,6,7,8,9,10};int i;int sum = 0;//存放数组和的变量for (i = 0; i < 10; i++) //想找最大的值,一定要把数组先遍历一遍{ sum += array[i]; } printf(“sum = %d\n”,sum); return 0;}将数组元素逆置
    int main(){ int array[10] = {32,5,67,98,12,54,8,78,457,10};/*int tmp = array[1]; //中间变量实现两个值的互换array[1] = array[0];array[0] = tmp;*/int min = 0; //数组最小下标int max = 9; //数组最大下标while (min < max) //两头往中间堵{ int tmp = array[min]; array[min] = array[max]; array[max] = tmp; min++; max--;} printf(“max = %d min = %d\n”, max , min); return 0;}求100到999之间的水仙花数
    int main(){ int i; for(i = 100 ;i < 1000 ;i++) { Int i1=i%10; // Int i2=i/10%10; // Int i3=i/100; // If((i1*i1*i1+i2*i2*i2+i3*i3*i3) = = i) Printf(“%d\n”,i);}return 0;}求一个int数组中,所有奇数元素的和
    int main(){ int array[10] = {1,2,3,4,5,6,7,8,9,10};int i;int sum = 0;for (i = 0; i < 10; i++) { if((array[i]%2) = = 1) { sum += array[i]; } } printf(“sum = %d\n”,sum); return 0;}求从3到100之间所有素数打印出来 3 5 7 11 13 17 ……
    int main(){int i; //素数是除了1和自己以外,不能被其他整数整除的整数for (i = 3; i < 100; i++) { int j; int ststus = 0; for(j =2 ;j < i ; j++) //判断i是否为素数 { if((i %j) = = 0) { status = 1; break;} } if(status= = 0) //代表这是个素数 { printf(“%d\n”,i);} } return 0;}7.4 二维数组定义与使用int array[2][3];//定义了一个二维数组,有两个array[3]int array[2][3] = { {1,2,3},{4,5,6} };//定义一个二维数组的同时初始化成员7.5 二维数组初始化int a[3][4] = {{1,2,3,4},{5,6,7,8},{9,10,11,12}};int array[2][3] = {0};//将二维数组中每个元素的值都初始化为0 int array[2][3] = { {1,2,3},{4,5,6} }; int i,j; for(j=0;j<3;j++) { int sum=0; for(i=0;i<2;i++) { sum += array[i][j]; } printf("%d\n",sum);//打印列的和 }#include<stdio.h>int main()//冒泡排序{ int array[10] = {34,14,8,54,23,89,56,4,45,22}; int i; int j; for(i = 0;i < 10; i++) { for(j = 1; j < 10 - i; j++) { if(array[j-1] > array[j])//如果前面的元素大于后面的元素,那么就让这两个元素交换位置 { int tmp = array[j]; array[j] = array[j - 1]; array[j-1] = tmp; } } } for(i=0;i<10;i++) { printf("array[%d]=%d\n",i,array[i]); } return 0;}8 字符串与字符数组字符串一定是在内存中以0结尾的一个char数组。
    8.1 字符数组定义char array[100];8.2 字符数组初始化char array[100] = {'a','b','c','d'};char array[100] = ”abcd”;char array[100] = {0};char array[] = “abcd”;8.3 字符数组使用#include<stdio.h>//字符串倒序int main(){ char buf[100] = "hello world"; int len = 0; while (buf[len++]); len--; int i = 0; int j = len -1; while (i < j) { char tmp = buf[i]; buf[i] = buf[j]; buf[j] = tmp; i ++; j --; } printf("%s\n",buf); return 0;}ASCII一个字节存放一个字符。GBK用两个字节存放一个汉字。
    8.4 随机数产生函数rand与srand头文件stdilb.hrand是伪随机数产生器,每次调用rand产生的随机数是一样的。如果调用rand之前先调用srand就出现任意的随机数。只要能保证每次调用srand函数的时候,参数的值是不同的,那么rand函数就一定会产生不同的随机数。
    Srand() //随机数种子发生器Rand() //随机数产生器#include<stdio.h>#include<time.h>#include<stdlib.h>int main(){ int t = (int)time(NULL); srand(t); for(int i=0; i<10; i++) { printf("%d\n",rand()); } return 0;}8.5 用scanf输入字符串“%s”的作用就是输入一个字符串的,scanf是以回车键作为输入完成标志的,但回车键本身并不会作为字符串的一部分如果scanf参数中的数组长度小于用户在键盘输入的长度,那么scanf就会缓冲区溢出,导致程序崩溃。
    8.6 字符串的结束标志scanf将回车、空格都认为是字符串输入结束标志。
    8.7 字符串处理函数8.7.1 getsgets(s);Gets认为回车是输入结束的标志,空格不是输入结束的标志,所以用gets这个函数就可以实现输入带空格的字符串,但是gets和scanf一样存在缓冲区溢出的问题。
    Gets不能用类似“%s”或者“%d”之类的字符转义,只能接受字符串的输入。
    8.7.2 fgets函数gets函数不检查预留缓冲区是否能够容纳用户实际输入的数据。多出来的字符会导致内存溢出,fgets函数改进了这个问题。由于fgets函数是为读取文件设计的,所以读取键盘时没有gets那么方便。
    Char s[100] = {0};fgets(s ,sizeof(s) ,stdin);//第一个参数是char的数组,第二个参数是数组的大小,单位:字节,第三个参数stdin代表标准输入的意思fgets是安全的,不存在缓冲区溢出的问题,调用fgets的时候,只要能保证第二个参数小于等于数组实际的大小,那么就可以避免缓冲区溢出的。
    8.7.3 puts函数Puts函数打印字符串,与printf不同,puts会在最后自动添加一个’\n’
    Char s[] = “hello world”;Puts(s);8.7.4 fputs函数fputs是puts的文本操作版本
    char s[] = “hello world”;fputs(s,stdout);8.7.5 strlen,字符串长度size_t strlen(const char * _str);返回不包含字符串结尾’\n’的字符串长度
    Char s[100] = “hello world”;Int len = strlen(s);Printf(“len = %d\n”,len);8.7.6 strcat,字符串追加size_t strcat(char * _str1,const char * _str2);将参数_str2追加到_str1后尾
    Char s1[100] = “fsfgg”;Strcat(s,s1);Strcat也存在缓冲区溢出的问题
    8.7.7 strncat,字符串有限追加size_t strncat(char * _str1,const char * _str2,size_t len);Strcat(s,s1,3); //合并的时候可以限制追加多少个字符8.7.8 strcmp,字符串比较int strcmp(const char * _str1,const char * _str2);比较两个字符串是否相等,相等返回0,不相等返回非0
    If(strcmp(s1,s2)) = = 0){ Printf(“相同\n”);}8.7.9 strncmp,字符串有限比较If(strncmp(s1,s2,5) = = 0) //strncmp的意思是只比较指定数量的字符8.7.10 strcpy字符串拷贝Char *strcpy(char * _str1,const char * _str2);将参数_str2拷贝到参数_str1中
    Strcpy(s1,s2);8.7.11 strncpy字符串有限拷贝Strncpy(s1,s2,3);8.7.12 sprintf格式化字符串和printf函数功能类似,printf函数将格式化结果输出到屏幕,sprintf将格式化结果输出到字符串。
    int i = 200;char s[100] = {0};sprintf(s, “I = %d”,i);8.7.13 Sscanf函数Sscanf类似于scanf函数,scanf从键盘读取用户输入,scanf从指定格式化字符串读取输入。
    8.7.14 strchr查找字符Char * strchr(char * _Str, int _Ch);在参数_str中查找参数_ch指定字符,找到返回字符_ch在_str中所在位置,没有找到返回NULL;
    8.7.15 strstr查找子串Char * strstr(char * _Str,const char * _SubStr);在参数_str中查找参数_SubStr指定子串,找到返回子串在_Str中所在位置,没有找到返回NULL;
    8.7.16 strtok分割字符串字符在第一次调用时strtok()必需给予参数s字符串,往后的调用则将参数s设置成NULL每次调用成功则返回指向被分割出片段的指针。
    如果strtok没有找到指定的分割符号,那么返回NULL
    Char buf [] = “abcd@efg@h”;Char *p = strtok(buf,”@”);While(p){ Printf(“%s\n”,p); P = strtok(NULL,”@”);}8.7.17 atoi转化为intint i1 = atoi(a); //将字符串转化为一个整数需要包含头文件stdlib.h
    8.7.18 atof转化为float8.7.19 atol转化为long#include <string.h> //计算字符串int calc_string(const char *s){ char buf1[100] = {0}; char oper1 = 0; char buf2[100] = {0}; int len = strlen(s);//得到字符串的长度 int i; int start; for(i=0;i<len;i++) { if(s[i] == '+'|| s[i] == '-' || s[i] == '*' || s[i] == '/') { strncpy(buf1, s, i); oper1 = s[i]; break; } } start = i + 1; for(;i<len;i++) { if(s[i] == '=') { strncpy(buf2,&s[start],i-start); } } printf("buf1 = %s,oper1 = %c,buf2 = %s\n",buf1,oper1,buf2); switch(oper1) { case '+': return atoi(buf1) + atoi(buf2); case '-': return atoi(buf1) - atoi(buf2); case '*': return atoi(buf1) * atoi(buf2); case '/': { int a = atoi(buf2); if(a) return atoi(buf1) / atoi(buf2); else return 0; } }}int main(){ const char *s = "32 + 56 ="; printf("%d\n",calc_string(s)); return 0;}9 函数9.1 函数的原型和调用在使用函数前必须定义或者声明函数
    9.2 函数的形参与实参在调用函数的时候,函数大多数都有参数,主调函数和被调用函数之间需要传递数据。在定义函数时函数名后面括弧中的变量名称为“形式参数”,简称形参。在调用函数时,函数名后面括号中的变量或表达式称为“实际参数”,简称实参。

    形参在未出现函数调用时,他们并不占用内存单元,只有在发生函数调用的时候形参才被分配内存,函数调用完成后,形参所占的内存被释放;
    实参可以是变量,常量或者表达式;
    在定义函数时,一定要指定形参的数据类型;
    形参与实参的数据类型一定要可兼容;
    在C语言中,实参与形参的数据传递是“值传递”,即单向传递,只由实参传递给形参,而不能由形参传递给实参。

    如果函数的参数是个数组,那么是可以通过形参修改实参的值的
    9.3 函数的返回类型与返回值
    函数的返回值通过函数中的return获得,如果函数的返回值为void可以不需要return语句;
    函数return语句中的返回值数据类型应该与函数定义时相同;
    如果函数中没有return语句,那么函数将返回一个不确定的值。

    如果C语言一个函数没有明确的标明函数的返回类型,那么函数的返回类型就是int;如果一个函数没有返回值,那么函数的返回类型是void;
    9.4 main函数与exit函数与函数的return语句exit(0); //在子函数中调用exit同样代表程序终止,但在子函数中调用return只是子函数终止,程序正常执行。exit是C语言的库函数,调用exit的结果就是程序终止,在main函数中调用exit与调用return是一样的;
    main函数return代表程序终止。
    9.5 多个源代码文件程序的编译9.5.1 头文件的使用如果把main函数放在第一个文件中,而把自定义函数放在第二个文件中,那么就需要在第一个文件中声明函数原型。如果把函数原型包含在一个头文件里,那么就不必每次使用函数的时候都声明其原型了。把函数声明放入头文件是很好的习惯。
    9.5.2 #include与#define的意义#include就是简单的文件内容替换#define就是简单的文件替换而已9.5.3 #ifndef 与#endif在头文件.h中,
    #ifndef _宏名_#define _宏名_//具体宏的名字是自定义的//函数的声明#endif作用:防止多次include的同一个头文件的时候,重复预编译头文件内容防止头文件被重复包含#ifndef的意思就是条件预编译,如果#ifndef后面的条件成立,那么就预编译从#ifndef开始到#endif之间的代码,否则不会去预编译这段代码。在#ifndef中的宏,一定要大写和下划线,必要的时候加数字,目的是为了避免和其他头文件中的宏名字冲突。#ifdef,#ifndef叫条件编译语句;#ifdef 宏,如果宏被定义了,那么编译语句;#ifndef 宏,如果宏被定义了,那么就不编译语句。9.6 函数的递归函数可以调用自己,这就叫函数的递归。
    #include <stdio.h>void test(int n){ if(n > 0) { n --; printf("先序n = %d\n",n);//先序递归,如果是先序递归,那么代码是顺序执行的 test(n);//函数自己调用自己,就叫函数的递归 printf("后序n = %d\n",n);//后序递归,如果是后序递归,那么代码是逆序执行的 }}int main(){ int i = 3; test(i); return 0;}9.6.1 递归的过程分析案例:将十进制转换为二进制
    #include <stdio.h>void test(int n){ int i = n % 2; printf("%d\n",i); if(n > 0) { test(n / 2); }}int main(){ int i = 11; test(i); return 0;}斐波那契数列例子:斐波那契数列指的是这样一个数列0,1,1,2,3,5,8,13,21,34,55,89,144,…第0项是0,第1项是第一个1;这个数列从第2项开始,每一项都等于前两项之和。
    int fib(int n){ if (n == 0) return 0; if (n == 1) return 1; else { return fib(n - 1) + fib(n - 2); }}9.6.2 递归的优点递归给某些编程问题提供了最简单的方法。
    9.6.3 递归的缺点一个有缺陷的递归会很快耗尽计算机的资源,递归的程序难以理解和维护。
    10 指针10.1 指针10.1.1 指针的概念指针变量也是一个变量;指针存放的内容是一个地址,该地址指向一块内存空间
    10.1.2 指针变量的定义可以定义一个指向一个变量的指针变量
    Int *p; //表示定义一个指针变量*p; //代表指针所指内存的实际数据切记,指针变量只能存放地址,不能将一个int型变量直接赋值给一个指针。Int *p = 100;Int *p = &a; //得到变量a的地址,将这个地址赋值给变量pInt *p1;//定义一个变量,名字叫p1,它可以指向一个int的地址P1=&b;//指针变量的值一般不能直接赋值一个整数,而是通过取变量地址的方式赋值10.1.3 &取地址运算符&可以取得一个变量在内存当中的地址
    10.1.4 无类型指针定义一个指针变量,但不指定它指向具体哪种数据类型。可以通过强制转化将void 转化为其他类型指针,也可以用(void )将其他类型指针强制转化为void类型指针。
    Void *p;10.1.5 NULLNULL在C语言中的定义为(void *)0;当一个指针不指向任何一个有效内存地址的时候,我们应该把指针设置为NULL。
    10.1.6 空指针与野指针指向NULL的指针叫空指针,没有具体指向任何变量地址的指针叫野指针。空指针是合法的,但野指针是危险的,是导致程序崩溃的主要原因。
    10.1.7 指针的兼容性指针之间赋值比普通数据类型赋值检查更为严格,例如:不可以把一个double *赋值给int *;原则上一定是相同类型的指针指向相同类型的变量地址,不能用一种类型的指针指向另一种类型的变量地址。
    10.1.8 指向常量的指针与指针常量Const char * p; //定义一个指向常量的指针Char *const p;//定义一个指针常量,一旦初始化之后其内容不可改变。10.1.9 指针与数组的关系一个变量有地址,一个数组包含若干个元素,每个元素在内存中都有地址。
    Int a[10];Int *p = a;比较p和&a[0]的地址是否相同。
    10.1.10 指针运算指针运算不是简单的整数加减法,而是指针指向的数据类型在内存中占用字节数作为倍数的运算。
    Char *p;P++; //移动了sizeof(char)这么多的字节数int *p1;P1++; //移动了sizeof(int)这么多的字节数赋值:int *p = &a;求值:int i = *p;取指针地址int **pp = &p;将一个整数加(减)给指针:p+3;p-3;增加(减少)指针值 p++,p--求差值,p1-p2,通常用于同一个数组内求两个元素之间的距离比较p1= =p2,通常用来 比较两个指针是否指向同一个位置10.1.11 通过指针使用数组元素P+1代表&a[1],也可以直接使用p[1]表示a[5]P +5代表&a[5];P++
    寻找数组第二大元素第一步:假设数组中前2个元素就是最大的和第二大的MaxSmax第二步:从数组的第2号元素开始遍历数组当有元素大于max的时候,Smax = maxMax= 最大的那个元素,如果当前元素小于max,并且大于smax,那么就让smax等于当前那个元素
    int smax(int *s)//求数组中第二大元素{ int max = 0; int s_max = 0; int i; if (*s > *(s +1)) { max = *s; s_max = *(s + 1); } else { max = *(s + 1); s_max = *s; }//将max等于s[0]和s[1]中大的那个元素的值 for(i = 2;i < 10;i++)//从第3个元素开始遍历数组 { if(max < *(s + i))//如果遇到大于max的元素,那么让s_max等于max,让max等于这个元素 { s_max = max; max = *(s + i); } else if(max > *(s + i) && *(s + i) > s_max)//如果这个元素是介于max和s_max之间,那么就让这个元素等于s_max { s_max = *(s + i); } } return s_max;//返回s_max的值}int main(){ int buf[10] = {34,21,56,4,87,90,15,65,72,48}; printf("%d\n",smax(buf)); return 0;}#include<string.h>//通过指针将字符串逆置int main(){ char str[100]="you"; char *str_start = &str[0]; char *str_end = &str[strlen(str) - 1]; while(str_start < str_end) { char *tmp = * str_start; * str_start = * str_end; * str_end = tmp; str_start ++; str_end --; } printf("%s\n",str); return 0;}对于VS的汉字是GBK编码,一个汉字2个字节;对于QT汉字是UTF8编码,一个汉字是3个字节。
    #include<string.h>//通过指针将汉字字符串逆置int main(){ char str[100]="我爱你"; short *str_start = &str[0]; short *str_end = &str[strlen(str) - 2]; while(str_start < str_end) { short *tmp = * str_start; * str_start = * str_end; * str_end = tmp; str_start ++; str_end --; } printf("%s\n",str); return 0;}10.1.12 指针数组int *a[10];//定义了一个指针数组,一共10个成员,其中每个成员都是int *类型10.1.13 指向指针的指针(二级指针)指针就是一个变量,既然是变量就也存在内存地址,所以可以定义一个指向指针的指针。通过二级指针修改内存的值;
    Int I = 10;Int *p1 = &I;Int **p2 = &p1;Printf(“%d\n”,**p2);以此类推可以定义3级甚至多级指针。C语言允许定义多级指针,但是指针级数过多会增加代码的复杂性,考试的时候可能会考多级指针,但是实际编程的时候最多用到3级指针,但是3级指针也不常用,一级和二级指针是大量使用的。
    10.1.14 指向二维数组的指针Int buf[3][5] 二维数组名称,buf代表数组首地址Int (*a)[5] 定义一个指向int[5]类型的指针变量aa[0],*(a+0),*a 0行,0列元素地址a+1 第1行首地址a[1],*(a+1) 第1行,0列元素地址a[1]+2,*(a+1)+2,&a[1][2] 第1行,2列元素地址*(a[1]+2),*(*(a+1)+2),a[1][2] 第1行,2列元素的值//二维数组的指针计算二维数组行列的平均值int main(){ int buf[3][5] = {{2,4,3,5,1},{7,2,6,8,1},{7,3,9,0,2}}; int i; int j; int sum; for(i = 0;i < 3;i ++) { sum = 0; for(j = 0;j < 5;j ++) { sum += (*(*(buf + i) + j)); //sum += buf[i][j]; } printf("%d\n",sum / 5); } for(i = 0;i < 5;i ++) { sum = 0; for(j = 0;j < 3;j ++) { sum += (*(*(buf + j) + i)); //sum += buf[j][i]; } printf("%d\n",sum / 3); } return 0;}10.1.15 指针变量作为函数的参数函数的参数可以是指针类型 *,它的作用是将一个变量的地址传送给另一个函数。通过函数的指针参数可以间接的实现形参修改实参的值。
    10.1.16 一维数组名作为函数参数当数组名作为函数参数时,C语言将数组名解释为指针
    int func(int array[10]);//数组名代表数组的首地址10.1.17 二维数组名作为函数参数二维数组做函数参数时可以不指定第一个下标。
    int func(int array[][10]);将二维数组作为函数的参数用例不是特别多见。
    10.1.18 const关键字保护数组内容如果讲一个数组作为函数的形参传递,那么数组内容可以在被调用函数内部修改,有时候不希望这样的事情发生,所以要对形参采用const参数。
    func(const int array[]);10.1.19 指针作为函数的返回值return NULL;10.1.20 指向函数的指针指针可以指向变量、数组,也可以指向一个函数。一个函数在编译的时候会分配一个入口地址,这个入口地址就是函数的指针,函数名称就代表函数的入口地址。函数指针的定义方式:
    int (*p)(int);//定义了一个指向int func(int n)类型函数地址的指针。
    定义函数指针变量的形式为:函数返回类型(*指针变量名称)(参数列表)
    函数可以通过函数指针调用

    int(* p)()代表指向一个函数,但不是固定哪一个函数Void *p(int ,char *);//声明了一个函数,函数的名字叫p,函数的返回值是void *,函数的参数是int和char *Void (*p)(int ,char *);//定义了一个指向参数为int和char *,返回值为void的函数指针Int *(*p)(int *);//定义一个参数为int *,返回值为int *的指向函数的指针Int *p(int *);//声明了一个函数,返回值是int *,参数是int *Int (*p)(int , int );//定义了一个指向函数的指针,可以指向两个参数,都是int,返回值也是int类型在回调函数和运行期动态绑定的时候大量的用到了指向函数的指针。
    10.1.21 把指向函数的指针作为函数的参数将函数指针作为另一个函数的参数称为回调函数。
    int max(int a,int b){ if(a > b) return a; else return b;}int add(int a,int b){ return a + b;}int func(int (*p)(int,int),int a,int b)//第一个参数是指向函数的指针{ return p(a,b);//通过指向函数的指针调用一个函数}int main(){ int i = func(add,6,9);//add函数在这里就叫回调函数 printf("i = %d\n",i); return 0;}10.1.22 memset,memcpy,memmove函数这三个函数分别实现内存设置,内存拷贝,内存移动使用memcpy的时候,一定要确保内存没有重叠区域。
    memset(buf, 0, sizeof(buf));//第一个参数是要设置的内存地址,第二个参数是要设置的值,第三个参数是内存大小,单位:字节memcpy(buf2, buf1, sizeof(buf1));//将buf1的内存内容全部拷贝到buf2,拷贝大小为第三个参数:字节memmove(buf2, buf1, sizeof(buf1));//并没有改变原始内存的值
    10.1.23 指针小结定义 说明Int I 定义整形变量Int *p 定义一个指向int的指针变量Int a[10] 定义一个int数组Int *p[10] 定义一个指针数组,其中每个数组元素指向一个int型变量的地址Int (*p)[10] 定义一个数组指针,指向int[10]类型的指针变量Int func() 定义一个函数,返回值为int型Int *func() 定义一个函数,返回值为int *型Int (*p)() 定义一个指向函数的指针,函数的原型为无参数,返回值为intInt **p 定义一个指向int的指针的指针,二级指针11 字符指针与字符串11.1 指针和字符串在C语言中,大多数字符串操作其实就是指针操作。
    Char s[] = “hello world”;Char *p = s;P[0] = ‘a’;11.2 通过指针访问字符串数组char buf[100] = "hello world";char *p = buf;//*(p + 5) = 'a';//p[5] = 'b';p += 5;*p = 'c';p[3] = ' ';printf("buf = %s\n",buf);11.3 函数的参数为char *void print_array(int *p,int n)//如果参数是一个int数组,那么就必须传递第二个参数用来标示数组的长度{ int i; for(i = 0; i <n; i++) { printf("p[%d] = %d\n", i, p[i]); }}void print_str(char *s)//如果参数是个字符串,那么就不需要包含第二个参数//因为字符串是明确的以‘\0’结尾的,所以在函数内部是有条件来作为循环终止依据的{ int i = 0; while(s[i]) { printf("%c",s[i++]); }}11.4 指针数组作为main函数的形参Int main(int argc, char *argv[]);main函数是操作系统调用的,所以main函数的参数也是操作系统在调用时候自动填写的argc代表命令行参数的数量argv代表命令行的具体参数,是char *类型的
    12 内存管理12.1 作用域一个C语言变量的作用域可以是代码块作用域,函数作用域或者文件作用域。代码块是{}之间的一段代码。出现在{}之外的变量,就是全局变量。
    12.1.1 auto自动变量一般情况下代码块内部定义的变量都是自动变量。当然也可以显示的使用aotu关键字
    12.1.2 register寄存器变量通常变量在内存当中,如果能把变量放到CPU的寄存器里面,代码执行效率会更高register int i;//建议,如果有寄存器空闲,那么这个变量就放到寄存器里面使用对于一个register变量,是不能取地址操作的
    12.1.3 代码块作用域的静态变量静态变量是指内存位置在程序执行期间一直不改变的变量,一个代码块内部的静态变量只能被这个代码块内部访问。static int i = 0;//静态变量,只初始化一次,而且程序运行期间,静态变量一直存在
    12.1.4 代码块作用域外的静态变量代码块之外的静态变量在程序执行期间一直存在,但只能被定义这个变量的文件访问Static int a=0;//一旦全局变量定义static,意思是这个变量只是在定义这个变量的文件内部全局有效
    12.1.5 全局变量全局变量的存储方式和静态变量相同,但可以被多个文件访问
    12.1.6 外部变量与extern关键字extern int i;
    12.1.7 全局函数和静态函数在C语言中函数默认都是全局的,使用关键字static可以将函数声明为静态
    12.2 内存四区12.2.1 代码区代码区code,程序被操作系统加载到内存的时候,所有的可执行代码都加载到代码区,也叫代码段,这块内存是不可以运行期间修改的。
    12.2.2 静态区所有的全局变量以及程序中的静态变量都存储到静态区,比较如下两段代码的区别:
    int a=0; int a=0;int main() static int b=0;{ int main() static int b=0; {printf(“%p,%p\n”,&a,&b); printf(“%p,%p\n”,&a,&b);return 0; retrun 0;} }int *getb() //合法的{ static int a=0; return &a;}12.2.3 栈区栈stack是一种先进后出的内存结构,所有的自动变量,函数的形参都是由编译器自动放出栈中,当一个自动变量超出其作用域时,自动从栈中弹出。
    对于自动变量,什么时候入栈,什么时候出栈,是不需要程序控制的,由C语言编译器实现栈不会很大,一般都是以K为单位的
    int *geta()//错误,不能将一个栈变量的地址通过函数的返回值返回{ int a = 0; return &a;}12.2.4 栈溢出当栈空间已满,但还往栈内存压变量,这个就叫栈溢出。对于一个32位操作系统,最大管理4G内存,其中1G是给操作系统自己用的,剩下的3G都是给用户程序的,一个用户程序理论上可以使用3G的内存空间。
    12.2.5 堆区堆heap和栈一样,也是一种在程序运行过程中可以随时修改的内存区域,但没有栈那样先进后出的顺序。堆是一个大容器,它的容量要远远大于栈,但是在C语言中,堆内存空间的申请和释放需要手动通过代码来完成。
    int *geta()//可以通过函数的返回值返回一个堆地址,但记得一定要free{ int *p=malloc(sizeof(int ));//申请了一个堆空间 return p;}Int main(){ Int *getp=geta(); *getp=100; free(getp);}12.3 堆的分配和释放操作系统在管理内存的时候,最小单位不是字节,而是内存页。
    12.3.1 mallocvoid * malloc(size_t _Size);int *p=(int *)malloc(sizeof(int)*10);//在堆中间申请内存,在堆中申请了一个10个int这么大的空间malloc函数在堆中分配参数_Size指定大小的内存,单位:字节,函数返回void *指针。Void getheap(int *p){ P=malloc(sizeof(int) * 10);}//getheap执行完以后,p就消失了,导致它指向的具体堆空间的地址编号也随之消失了Void getheap1(int **p){ *p=malloc(sizeof(int) * 10);}Int main(){ Int *p=NULL; Printf(“p=%p\n”,&p); //getheap(p);//实参没有任何改变 getheap1(&p);//得到了堆内存的地址}12.3.2 freeVoid free(void *p);free(p);//释放通过malloc分配的堆内存free负责在堆中释放malloc分配的内存。参数p为malloc返回的堆中的内存地址。Int *array = malloc(sizeof(int) * i);//在堆当中动态创建一个int数组free(array);12.3.3 callocVoid * calloc(size_t _Count, size_t _Size);Calloc与malloc类似,负责在堆中分配内存。
    第一个参数是所需内存单元数量,第二个参数是每个内存单元的大小(单位:字节),calloc自动将分配的内存置0
    Int *p=(int *)calloc(100,sizeof(int));//分配100个int12.3.4 realloc重新分配用malloc或者calloc函数在堆中分配内存空间的大小。
    Void * realloc(void *p, size_t _NewSize);第一个参数p为之前用malloc或者calloc分配的内存地址,_NewSize为重新分配内存的大小,单位:字节。成功返回新分配的堆内存地址,失败返回NULL;如果参数p等于NULL,那么realloc与malloc功能一致。
    Char *p = malloc(10);//分配空间,但原有数据没做清洁Char *p1 = calloc(10, sizeof(char));//分配空间以后,自动做清洁Char *p2 =realloc(p1,20);//在原有内存基础之上,在堆中间增加连续的内存//如果原有内存没有连续空间可扩展,那么会新分配一个空间,将原有内存copy到新空间,然后释放原有内存。//realloc和malloc,只分配内存,但不打扫Char *p2 = realloc(NULL,5);//等于malloc(5)13 结构体,联合体,枚举与typedef13.1 结构体13.1.1 定义结构体struct和初始化Struct man{ Char name[100]; Int age;};Struct man m = {“tom”,12};Struct man m = {.name = “tom”, .age = 12};#include <string.h>#pragma warning(disable:4996)struct student{ char name[100]; int age; int sex;};//说明了一个结构体的数据成员类型int main(){ struct student st;//定义了一个结构体的变量,名字叫st; st.age = 20; st.sex = 0; strcpy(st.name,"刘德华"); printf("name = %s\n",st.name); printf("age = %d\n",st.age); if(st.sex == 0) { printf("男"); } else { printf("女"); } return 0;}13.1.2 访问结构体成员.操作符
    13.1.3 结构体的内存对齐模式编译器在编译一个结构的时候采用内存对齐模式。
    Struct man{ Char a; Int b; Shor c; Char d; Long long e;};13.1.4 指定结构体元素的位字段定义一个结构体的时候可以指定具体元素的位长;
    Struct test{ char a : 2;//指定元素为2位长,不是2个字节长};13.1.5 结构数组Struct man m[10] = {{“tom”,12},{“marry”,10},{“jack”,9}};Int I;for(i=0; i<5; i++){ Printf(“姓名=%s,年龄=%d\n”,m[i].name,m[i].age);}#include <stdio.h>#include <string.h>#pragma warning(disable:4996)//结构体数组排序struct student{ char name[100]; int age; int score; char classes[100];};void swap(struct student *a,struct student *b){ struct student tmp = *a; *a = *b; *b = tmp;}int main(){ struct student st[5] = {{"chen",12,78,"A"},{"li",10,90,"B"},{"wang",13,59,"C"},{"fei",12,91,"D"},{"bai",9,59,"E"}}; int i; int j; for(i = 0; i < 5; i++) { for(j = 1; j < 5-i; j++) { if(st[j].age < st[j - 1].age) { swap(&st[j],&st[j - 1]); } else if(st[j].age == st[j - 1].age) { if(st[j].score <st[j-1].score) { swap(&st[j],&st[j - 1]); } } } } for(i = 0; i < 5; i++) { printf("姓名=%s,年龄=%d,成绩=%d,班级=%s\n",st[i].name,st[i].age,st[i].score,st[i].classes); } return 0;}13.1.6 嵌套结构一个结构的成员还可以是另一个结构类型
    Struct names{ Char a; Int b;};Struct man{ Struct names name; Int c;};Struct man m = {{“wang”,10},20};13.1.7 结构体的赋值Struct name m = b;
    结构体赋值,其实就是结构体之间内存的拷贝
    13.1.8 指向结构体的指针—>操作符Struct A a;Struct A *p = &a;p->a = 10;13.1.9 指向结构体数组的指针13.1.10 结构中的数组成员和指针成员一个结构中可以有数组成员,也可以有指针成员,如果是指针成员结构体成员在初始化和赋值的时候就需要提前为指针成员分配内存。
    Struct man{ Char name[100]; Int age;};Struct man{ Char *name; Int age;};13.1.11 在堆中创建的结构体如果结构体有指针类型成员,同时结构体在堆中创建,那么释放堆中的结构体之前需要提前释放结构体中的指针成员指向的内存。
    Struct man{ Char *name; Int age;};Struct man *s = malloc(sizeof(struct man) * 2);S[0].name = malloc(10 * sizeof(char));S[1].name = malloc(10 * sizeof(char));13.1.12 将结构作为函数参数将结构作为函数参数将结构指针作为函数参数
    Struct student{ Char name[10]; Int age;};Void print_student (struct student s)//一般来讲,不要把结构变量作为函数的参数传递,因为效率比较低,一般用指针来代替//void print_student(const struct student *s){ Printf(“name = %s, age = %d\n”,s.name,s.age);}Int main(){ Struct student st = {“tom”,20}; Print_student(st);}13.1.13 结构,还是指向结构的指针在定义一个和结构有关的函数,到底是使用结构,还是结构的指针?指针作为参数,只需要传递一个地址,所以代码效率高。
    Void set_student (struct student *s, const char *name, int age){ Strcpy(s->name, name); s->age = age;}Int main(){ Set_student(&st, “maik”,100); Print_student(st);}结论:当一个结构作为函数的参数时候,尽量使用指针,而不是使用结构变量,这样代码效率很高。
    13.2 联合体联合union是一个能同一个存储空间存储不同类型数据的类型。联合体所占的内存长度等于其最长成员的长度,也有叫做共用体。联合体虽然可以有多个成员,但同一时间只能存放其中一种。
    union A{ Int a; Char b; Char *c; //注意};Int main(){ Union A a; a.a = 10;a.c = malloc(100);//c指向了一个堆的地址 free(c);//如果联合体中有指针成员,那么一定要使用完这个指针,并且free指针之后才能使用其他成员 a.b = 20;}13.3 枚举类型13.3.1 枚举定义可以使用枚举声明代表整数常量的符号名称,关键字enum创建一个新的枚举类型。实际上,enum常量是int类型的。枚举是常量,值是不能修改的。
    enum spectrum{ red,yellow,green,blue,white,black};enum spectrum color;color = black;if(color != red)13.3.2 默认值默认时,枚举列表中的常量被指定为0,1,2等
    enum spectrum{ red,yellow,green,blue,white,black};printf(“%d,%d\n”,red,black);指定值可以指定枚举中具体元素的值
    enum spectrum{ red=1,yellow=2,green=3,blue,white,black};13.4 typedeftypedef是一种高级数据特性,它能使某一类型创建自己的名字。
    typedef unsigned char UBYTE;typedef char BYTE;
    与#define不同,typedef仅限于数据类型,而不是能是表达式或具体的值
    typedef是编译器处理的,而不是预编译指令;
    typedef比#define更灵活

    直接看typedef好像没什么用处,使用BYTE定义一个unsigned char。使用typedef可以增加程序的可移植性。
    typedef struct{ Int a;}A2;A2 a;13.5 通过typedef定义函数指针typedef const char *(*SUBSTR)(const char *,const char *);const char *getsubstr(const char *src, const char *str){ return strstr(src,str);}Const char *(*p[3])(const char *,const char *);14 文件操作14.1 fopenChar s[1024] = {0};FILE *p = fopen(“D:\\temp\\a.txt”,”w”);//用写的方式打开一个文件fputs(“hello world”,p);//向文件写入一个字符串//feof(p);//如果已经到了文件结尾,函数返回真While(!feof(p))//如果没有到文件结尾,那么就一直循环{ memset(s,0,sizeof(s));//fgets(s,sizeof(s),p);//第一个参数是一个内存地址,第二个参数是这块内存的大小,第三个参数是fopen返回的文件指针printf(“%s”,s);}fclose(p);//关闭这个文件r以只读方式打开文件,该文件必须存在r+ 以可读写方式打开文件,该文件必须存在rb+ 读写打开一个二进制文件,允许读写数据,文件必须存在rw+ 读写打开一个文本文件,允许读和写w 打开只写文件,若文件存在则文件长度清0,即该文件内容会消失。若文件不存在则建立该文件;w+ 打开可读写文件,若文件存在则文件长度清0,即该文件内容会消失。若文件不存在则建立该文件;a 以附加的方式打开只写文件。若文件不存在,则会建立该文件,如果文件存在,写入的数据会被加到文件尾,即文件原先的内容会被保留。(EOF符保留)a+ 以附加的方式打开可读写文件。若文件不存在,则会建立该文件,如果文件存在,写入的数据会被加到文件尾,即文件原先的内容会被保留。(原来的EOF符不保留)。
    Void code(char *s){ While(*s)//遍历一个字符串 { (*s)++; s++;}}Int main()//文件加密{ Char s[1024] = {0};FILE *p = fopen(“D:\\temp\\a.txt”,”r”);//用读的方式打开一个文件FILE *p1 = fopen(“D:\\temp\\a.txt”,”w”);//用写的方式打开一个文件//feof(p);//如果已经到了文件结尾,函数返回真While(!feof(p))//如果没有到文件结尾,那么就一直循环{ memset(s,0,sizeof(s));fgets(s,sizeof(s),p);//第一个参数是一个内存地址,第二个参数是这块内存的大小,第三个参数是fopen返回的文件指针code(s);//文件加密fputs(s,p1);}fclose(p);//关闭这个文件fclose(p1);}14.2 二进制和文本模式的区别
    在windows系统中,文本模式下,文件以“\r\n”代表换行。若以文本模式打开文件,并用fputs等函数写入换行符”\n”时,函数会自动在”\n”前面加上“\r”。即实际写入文本的是”\r\n”。
    在类Unix/Linux系统中文版模式下,文件以”\n”代表换行。所以Linux系统中在文本模式和二进制模式下并无区别。

    14.3 fclosefclose关闭fopen打开的文件
    14.4 getc和putc函数Int main() int main(){ { FILE *fp=fopen(“a.txt”,”r”); FTLE *fp=fopen(“a.txt”,”w”); Char c; const char *s=”hello world”; While((c=getc(fp))!=EOF) int I; { for(i=0;i<strlen(s);i++) Printf(“%c”,c); {} putc(s[i],fp);fclose(fp); }return 0; fclose(fp);} return 0; }14.5 EOF与feof函数文件结尾程序怎么才能知道是否已经到达文件结尾了呢?EOF代表文件结尾如果已经是文件尾,feof函数返回true。
    While((c=getc(p))!=EOF)//EOF代表文件最后的一个结束标识{ //c=getc(p);//一次只读取一个字符 Printf(“%c”,c);}##14.6 fprintf,fscanf,fgets,fputs函数这些函数都是通过FILE *来对文件进行读写。都是针对文本文件的行读写函数fprintf(p,”%s”,buf);//和printf功能一样,fprintf将输入的内容输入到文件里面fscanf(p,”%s”,buf); //fscanf与scanf用法基本一致,fscanf是从一个文件读取输入,scanf是从键盘读取输入#include <stdio.h>#include <stdlib.h>#include <string.h>//计算文本中的字符串int calc_string(const char *s){ char buf1[100] = {0}; char oper1 = 0; char buf2[100] = {0}; int len = strlen(s);//得到字符串的长度 int i; int start; for(i=0;i<len;i++) { if(s[i] == '+'|| s[i] == '-' || s[i] == '*' || s[i] == '/') { strncpy(buf1, s, i); oper1 = s[i]; break; } } start = i + 1; for(;i<len;i++) { if(s[i] == '=') { strncpy(buf2,&s[start],i-start); } } printf("buf1 = %s,oper1 = %c,buf2 = %s\n",buf1,oper1,buf2); switch(oper1) { case '+': return atoi(buf1) + atoi(buf2); case '-': return atoi(buf1) - atoi(buf2); case '*': return atoi(buf1) * atoi(buf2); case '/': { int a = atoi(buf2); if(a) return atoi(buf1) / atoi(buf2); else return 0; } }}void cutereturn(char *s)//把字符串最后的回车字符吃掉{ int len = strlen(s); if(s[len - 1] == '\n') s[len - 1] = 0;}int main(){ FILE *p = fopen("D:\\main\\a.txt","r");//32+56=88 FILE *p1 = fopen("D:\\main\\b.txt","w"); char buf[1024]; char buf1[1024]; int value; while(!feof(p)) { memset(buf,0,sizeof(buf)); fgets(buf,sizeof(buf),p);//从文件中读取一行记录,字符串最后是以'\n'结尾的 cutereturn(buf);//吸收回车 value = calc_string(buf); memset(buf1,0,sizeof(buf1)); sprintf(buf1,"%s%d\n",buf,value);//将buf和buf计算结果重新组织成一个字符串 fputs(buf1,p1);//将重新组合后的字符串写入新的文件 } fclose(p); fclose(p1); return 0;}14.7 stat函数#include <sys/stat.h>int stat(const char *_Filename, struct stat * _Stat);stat.st_size;//文件大小,单位:字节函数的第一个参数代表文件名,第二个参数是struct stat结构。得到文件的属性,包括文件建立时间,文件大小等信息。Struct stat st = {0};//定义一个结构,名字叫stStat(“D:\\temp\\a.txt”,&st);//调用完stat函数之后,文件相关的信息就保存在了st结构中了14.8 fread和fwrite函数size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);size_t fwrite(const void *ptr, size_t size, size_t nmemb,FILE *stream);注意:这个函数以二进制形式对文件进行操作,不局限于文本文件,可进行二进制的读写和拷贝返回值:返回实际写入的数据块数目size_t res=fread(buf,sizeof(char),sizeof(buf),p);//第一个参数是缓冲区,第二个参数是读取的时候最小单位大小,第三个参数是一次读几个单位,第四个参数是打开的文件指针,fread返回值代表读取了多少记录数fwrite(buf,sizeof(char),res,p1);//从源文件中读取多少字节,那么就往目标文件中写多少字节14.9 fread与feofwhile(!feof(p))//如果没有到达文件结尾,那么循环继续{ memset(buf,0,sizeof(buf));//每次读取文件一行之前都把这个buf清空 fgets(buf,sizeof(buf),p);//从文件中读取一行 fread(&buf, 1, sizeof(buf), p);}14.10 通过fwrite将结构保存到二进制文件中做一个代码例子
    14.11 fseek函数int fseek(FILE *stream, long offset, int whence);函数设置文件指针stream的位置。如果执行成功,stream将指向以fromwhere为基准,偏移offset(指针偏移量)个字节的位置,函数返回0。如果执行失败则不改变stream指向的位置,函数返回一个非0值。实验得出,超出文件末尾位置,还是返回0。往回偏移超出首位置,还是返回0,请小心使用。第一个参数stream为文件指针;第二个参数offest为偏移量,正数表示正向偏移,负数表示负向偏移;第三个参数whence设定从文件的哪里开始偏移,可能取值为:SEEK_CUR、SEEK_END或SEEK_SET。SEEK_SET:文件开头SEEK_CUR:当前位置SEEK_END:文件结尾fseek(fp, 3, SEEK_SET);
    14.12 ftell函数long ftell(FILE *stream);函数ftell用于得到文件位置指针当前位置相对于文件首的偏移字节数。在随机方式存取文件时,由于文件位置频繁的前后移动,程序不容易确定文件的当前位置。
    long len = ftell(fp);14.13 fflush函数int fflush(FILE *stream);fflush函数可以将缓冲区中任何未写入的数据写入文件中。成功返回0,失败返回EOF。每当程序通过C语言库函数往文件里面写数据,C语言库函数并不是实时的将数据直接写入磁盘,而是放到内存里面,当内存满了或者明确的调用了fclose,才将数据一次性写入磁盘。结合:C语言所有的文件操作函数都是缓冲区函数。
    fflush(p);//fflush将缓冲区的内容立刻写入文件//优势:不会因为停电,或者电脑死机等故障导致缓冲区的内容丢失;//不好:硬盘读写次数增加,导致程序效率低下,同时硬盘寿命变短。修改配置文件的时候,有时候会使用,或者做一些不经常修改的数据,但很重要数据,那么用fflush。
    14.14 remove函数int remove(const char *_Filename);remove函数删除指定文件参数Filename为指定的要删除的文件名,如果是windows下文件名与路径可以用反斜杠\分隔,也可以用斜杠/分隔。
    14.15 rename函数int rename(const char *_OldFilename, const char *_NewFilename);rename函数将指定文件改名参数OldFilename为指定的要修改的文件名,NewFilename为修改后的文件名,如果是windows下文件名与路径可以用反斜杠\分隔,也可以用斜杠/分隔。
    //程序还没有退出的时候,是不能同时打开很多文件的一个程序同时可以打开的文件数是有限的尽量在一个程序中不要同时打开太多的文件,如果确实要操作很多文件,也是一个操作完毕fclose以后,再去操作下一个文件。
    15 基础数据结构与算法15.1 什么是数据结构数据(data)是对客观事物符号表示,在计算机中是指所有能输入的计算机并被计算机程序处理的数据总称。数据元素(data element)是数据的基本单位,在计算机中通常作为一个整体进行处理。数据对象(data object)是性质相同的数据元素的集合,是数据的一个子集。数据结构(data structure)是相互之间存在一种或多种特定关系的数据元素的集合。数据类型(data type)是和数据结构密切关系的一个概念,在计算机语言中,每个变量、常量或者表达式都有一个所属的数据类型。抽象数据类型(abstract data type ADT)是指一个数据模型以及定义在该模型上的一组操作,抽象数据类型的定义仅取决于它的一组逻辑性,与其在计算机内部如何表示以及实现无关。
    15.2 什么是算法算法是对特定问题求解的一种描述,它是指令的有限序列,其每一条指令表示一个或多个操作,算法还有以下特性:

    有穷性一个算法必须总是在执行有限步骤后的结果,而且每一步都可以在有限时间内完成。
    确定性算法中每一个指令都有确切的含义,读者理解时不会产生二义性,在任何条件下,算法只有唯一的一条执行路径,即相同的输入只能得出相同的
    可行性一个算法是可行的,即算法中描述的操作都是可以通过已经实现的基本运算来实现的。
    输入一个算法有零个或者多个输入,这些输入取自与某个特定对象的集合。
    输出一个算法一个或多个输出,这些输出是和输入有某些特定关系的量。

    15.3 排序15.3.1 冒泡排序冒泡排序首先将一个记录的关键字和第二个记录的关键字进行比较,如果为逆序(elem[1]>elem[2]),则两个记录交换之,然后比较第二个记录和第三个记录的关键字,以此类推,直到第n-1个记录和第n个记录的关键字进行过比较为止。上述过程称作第一次冒泡排序,其结果是将关键字最大的记录接安排到最后一个记录的位置上,然后进行第二次冒泡排序,对前n-1个记录进行同样操作,其结果是使关键字第二大记录被安置到第n-1位置上,直到将所有记录都完成冒泡排序为止。
    #include <stdio.h>//冒泡排序void swap(int *a,int *b){ int tmp = *a; *a = *b; *b = tmp;}void bubble(int *array, int n){ int i; int j; for(i = 0; i < n; i++) for(j = 1; j < n-i; j++) { if(array[j - 1] > array[j]) { swap(&array[j - 1],&array[j]); } }}void print(int *array, int n){ int i; for(i = 0; i < n; i++) { printf("%d\n",array[i]); }}int main(void){ int array[10] = {32,45,8,78,21,89,4,15,23,56}; bubble(array,10); print(array,10); return 0;}15.3.2 选择排序选择排序是每一次在n-1+1(i=1,2,3,…n)个记录中选取关键字,最小的记录作为有序序列中第i个记录。通过n-1次关键字间的比较,从n-i+1个记录中选取出关键字最小的记录,并和第i(1<=i<=n)个记录交换之。
    int minkey(int *array, int low, int high)//查找指定范围内的最小值//第一个参数是一个数组,第二个参数是数组的开始下标,第三个参数是数组的终止下标//返回值是最小元素的下标{ int min = low; int key = array[low];//在没有查找最小元素之前,第一个元素是最小的 int i; for(i = low + 1; i < high; i++) { if(key > array[i]) { key = array[i]; min = i; } } return min;}void select(int *array, int n)//选择排序法{ int i; for(i = 0; i < n; i++) { int j = minkey(array, i, n); if(i != j)//范围内的第一个成员不是最小的 { swap(&array[i],&array[j]); } }}int main(void){ int array[10] = {32,45,8,78,21,89,4,15,23,56}; select(array,10); return 0;}15.4 查找15.4.1 顺序查找顺序查找的过程为:从表的最后一个记录开始,逐个进行记录的关键字和给定值比较,如果某个记录的关键字与给定值相等,则查找成功,反之则表明表中没有所查找记录,查找失败。
    int seq(int *array, int low, int high, int key)//顺序查找//在指定范围内寻找和key相同的值,找到返回下标,找不到返回—1{ int i; for(i = low; i < high; i++) { if(array[i] == key) return i; } return -1;}15.4.2 二分查找在一个已经排序的顺序表中查找,可以使用二分查找来实现。二分查找的过程是:先确定待查记录所在的范围(区间),然后逐步缩小查找范围,直到找到或者找不到该记录为止。假设指针low和high分别指示待查找的范围下届和上届,指针mid指示区间的中间值,即mid=(low + high) / 2。
    int bin(int *array, int low, int high, int key)//二分查找{ while(low <= high) { int mid = (low + high) / 2; if(key == array[mid])//中间切一刀,正好和要查找的数相等 return mid; else if(key > array[mid])//如果要找的数大于array[mid],那么就在下半部分继续切刀 low = mid + 1; else//如果要找的数小于array[mid],那么就在上半部分继续切刀 high = mid - 1; } return -1;//没有找到数据}int bin_rec(int *array, int low, int high, int key)//递归法实现二分查找{ if(low <= high) { int mid = (low + high) / 2; if(key == array[mid])//中间切一刀,正好和要查找的数相等 return mid; else if(key > array[mid])//下半部分继续查找 return bin_rec(array,mid + 1,high,key); else return bin_rec(array,low,mid - 1,key);//在上半部分查找 }else return -1;//没有找到数据}15.5 链表15.5.1 单向链表定义对于数组,逻辑关系上相邻的两个元素的物理位置也是相邻的,这种结构的优点是可以随机存储任意位置的元素,但缺点是如果从数组中间删除或插入元素时候,需要大量移动元素,效率不高。
    链式存储结构的特点,元素的存储单元可以是连续的,也可以是不连续的,因此为了表示每个元素a,与其接后的元素a+1之间的关系,对于元素a,除了存储其本身信息外,还需要存储一个指示其接后元素的位置。这两部分数据成为结点(node)。一个结点中存储的数据元素被称为数据域。存储接后存储位置的域叫做指针域。N个结点(ai(1<=i<=n))的存储映像链接成一个链表。
    整个链表必须从头结点开始进行,头结点的指针指向下一个结点的位置,最后一个结点的指针指向NULL;在链表中,通过指向接后结点位置的指针实现将链表中每个结点“链”到一起。链表中第一个结点称之为头结点。N个结点(ai的存储映像)链结成一个链表,即为线性表(a1,a2,…,an)的链式存储结构,因为此链表的每个结点中只包含一个指针域,所以叫做单链表。单链表正是通过每个结点的指针域将线性表的数据元素按其逻辑次序链接在一起。如图所示:
    15.5.2 单向链表数据结构定义struct list{ int data;//数据域 struct list *next;//指针域};15.5.3 单向链表的实现#include <stdio.h>#include <stdlib.h>struct list{ int data;//数据域 struct list *next;//指针域};struct list *create_list()//建立一个节点{ return calloc(sizeof(struct list),1);}struct list *insert_list(struct list *ls, int n, int data)//在指定位置插入元素{ struct list *p = ls; while(p && n--) { p = p->next; } if(p == NULL) { return NULL;//n的位置大于链表节点数 } struct list *node = create_list();//新建立一个节点 node->data = data; node->next = p->next; p->next = node; return node;}int delete_list(struct list *ls, int n)//删除指定位置元素{ struct list *p = ls; while(p && n--) { p = p->next; } if(p == NULL) { return -1;//n的位置不合适 } struct list *tmp = p->next; p->next = p->next->next; free(tmp); return 0;//删除成功}int count_list(struct list *ls)//返回链表元素个数{ struct list *p = ls; int count = 0; while(p) { count ++; p = p->next; } return count;}void clear_list(struct list *ls)//清空链表,只保留首节点{ struct list *p = ls->next; while(p) { struct list *tmp = p->next; free(p); p = tmp; } ls->next = NULL;//只有首节点,那么首节点的next也应该设置为NULL}int empty_list(struct list *ls)//返回链表是否为空{ if(ls->next) return 0; else return -1;}struct list *locale_list(struct list *ls, int n)//返回链表指定位置的节点{ struct list *p = ls; while(p && n--) { p = p->next; } if(p == NULL) return NULL; return p;}struct list *elem_locale(struct list *ls,int data)//返回数据域等于data的节点{ struct list *p = ls; while(p) { if(p->data == data) return p; p = p->next; } return NULL;//没有找到数据域等于data的节点}int elem_pos(struct list *ls, int data)//返回数据域等于data的节点位置{ int index = 0; struct list *p = ls; while(p) { index++; if(p->data == data) return index; p = p->next; } return -1;//没有找到数据域等于data的节点}struct list *last_list(struct list *ls)//得到链表最后一个节点{ struct list *p = ls; while(p->next) { p = p->next; } return p;}void merge_list(struct list *ls1,struct list *ls2)//合并两个链表,结果放入ls1中{ //只合并链表的节点,不合并链条头 last_list(ls1)->next = ls2->next; free(ls2);//链表头不要了}void reverse(struct list *ls)//链表逆置{ if (ls->next == NULL) return;//只有一个首节点,不需要逆置 if (ls->next->next == NULL) return;//也不需要逆置 struct list *last = ls->next;//逆置后ls->next就成了最后一个节点了 struct list *pre = ls;//上一个节点的指针 struct list *cur = ls->next;//当前节点的指针 struct list *next = NULL;//下一个节点的指针 while(cur) { next = cur->next; cur->next = pre; pre = cur; cur = next; } ls->next = pre; last->next = NULL;}void traverse(struct list *ls)//循环遍历链表{ struct list *p = ls; while(p) { printf("%d\n",p->data); p = p->next;//p指向他对应的下一个节点 }}int main(void){ struct list *first = create_list();//在堆中间创建一个节点 struct list *second = create_list();//在堆中间创建一个节点 struct list *third = create_list();//在堆中间创建一个节点 first->next = second; second->next = third; third->next = NULL;//对于链表的最后一个节点,next域一定为NULL first->data = 1; second->data = 2; third->data = 3; insert_list(first, 1, 10); insert_list(first, 1, 20); //delete_list(first, 2); //clear_list(first); traverse(first); printf("--------------\n"); printf("count = %d\n", count_list(first)); printf("%d\n", locale_list(first,3)->data); printf("data = %d\n",last_list(first)->data); printf("--------------\n"); struct list *first1 = create_list(); int i; for(i = 0; i < 10; i++) { insert_list(first1, 0, i); } merge_list(first,first1); printf("--------------\n"); traverse(first); printf("--------------\n"); reverse(first); traverse(first); return 0;}逆置操作

    判断首节点的next是否为NULL;
    判断第二个节点的next是否为NULL;
    逆置后ls->next就成了最后一个节点了
    最后一个节点的next指向NULL。
    0 留言 2019-07-27 10:48:37 奖励12点积分
  • windows下静态使用QxOrm框架并使用泛型编程 (三)

    这篇讲如何整合所有的表并且数据库增加字段升级,首先我们需要一张可以记录版本号的表
    VersionObject 这个类放置在QxObject里 因为字段不会增加所以我们只需要VersionHandler类即可 不需要映射类
    代码如下:VersionObject .h
    #ifndef VERSIONOBJECT_H#define VERSIONOBJECT_H#include "common.h"#include <QxOrm.h>#include <QString>class VersionObject{public: VersionObject(); void init();public: QString name; //long name; long version;};QX_REGISTER_PRIMARY_KEY(VersionObject, QString) //主键不是整数类型的时候使用QX_REGISTER_HPP_IMPORT_DLL(VersionObject, qx::trait::no_base_class_defined, DATABASE_VERSION)#endif // VERSIONOBJECT_HVersionObject .cpp
    #include "VersionObject.h"QX_REGISTER_CPP_IMPORT_DLL(VersionObject)namespace qx{template <> void register_class(QxClass<VersionObject> & t){ t.id(& VersionObject::name, "name"); t.data(& VersionObject::version, "version");}}VersionObject::VersionObject(){}void VersionObject::init(){ this->name= "qxorm"; this->version=DATABASE_VERSION;}VersionHandler.h
    #ifndef VERSIONHANDLER_H#define VERSIONHANDLER_H#include <QxOrm.h>#include "IHandler.h"#include "SQLModule/QxObject/VersionObject.h"namespace VERSION{const QString DATABASE_TYPE="QSQLITE";const QString CONNECT_NAME="VERSION_CONNECTED";const QString DATABASENAME="C:/Users/we/Desktop/workTools/demo/version.db";const QString HOSTNAME="localhost";const QString USERNAME="root";const QString PASSWORD="";}using namespace VERSION;class VersionObject;typedef QSharedPointer<VersionObject> Shared_Version;typedef QList<VersionObject> List_Version; //User类数组typedef qx::QxCollection<int,VersionObject> Collection_Version; //User容器class VersionHandler:public IHandler<Shared_Version,Collection_Version,VersionObject>,public ISqlInterface{public: VersionHandler(); virtual ~VersionHandler();protected: virtual void initSqlconnect(); virtual bool createTable(); virtual void disconnect();public: bool insert(Shared_Version &t); bool update(Shared_Version &t); bool select(Shared_Version &t);private: QSqlDatabase m_SqlDatabase; QMutex m_Mutex;};#endif // VERSIONHANDLER_HVersionHandler.cpp
    #include "VersionHandler.h"VersionHandler::VersionHandler(){ initSqlconnect(); if(createTable()) Shared_Version ersion(new VersionObject()); version->init(); this->insert(version); }}VersionHandler::~VersionHandler(){ disconnect();}void VersionHandler::initSqlconnect(){ QMutexLocker locker(&m_Mutex); if(QSqlDatabase::contains(CONNECT_NAME)) m_SqlDatabase = QSqlDatabase::database(CONNECT_NAME); else m_SqlDatabase= QSqlDatabase::addDatabase(DATABASE_TYPE,CONNECT_NAME); m_SqlDatabase.setDatabaseName(DATABASENAME); m_SqlDatabase.setHostName(HOSTNAME); m_SqlDatabase.setUserName(USERNAME); m_SqlDatabase.setPassword(PASSWORD); m_SqlDatabase.open();}bool VersionHandler::createTable(){ return IHandler<Shared_Version,Collection_Version,VersionObject>::createTable(m_SqlDatabase);}void VersionHandler::disconnect(){ QMutexLocker locker(&m_Mutex); if(m_SqlDatabase.isOpen()) m_SqlDatabase.close(); QSqlDatabase::removeDatabase(CONNECT_NAME);}bool VersionHandler::insert(Shared_Version &t){ return IHandler<Shared_Version,Collection_Version,VersionObject>::insert(t,m_Mutex,m_SqlDatabase);}bool VersionHandler::update(Shared_Version &t){ QStringList list; return IHandler<Shared_Version,Collection_Version,VersionObject>::update(t,m_Mutex,m_SqlDatabase,list);}bool VersionHandler::select(Shared_Version &t){ QStringList list; return IHandler<Shared_Version,Collection_Version,VersionObject>::select(t,m_Mutex,m_SqlDatabase,list);}为了更好的管理整个数据库的所有表 于是我们需要一个单例类来管理所有的Handler 所以我在SQLModule下新建了一个SQLHelper单例类
    代码如下:
    SqlHelper.h
    #ifndef SQLHELPER_H#define SQLHELPER_H#include <QMutex>#include "SQLModule/QxHandler/UserHandler.h"#include "SQLModule/QxHandler/VersionHandler.h"#include <QSqlDatabase>namespace SQLHELPER //用于版本升级的{const QString DATABASE_TYPE="QSQLITE";const QString CONNECT_NAME="UPDATE_ALLTABLE";const QString DATABASENAME="C:/Users/we/Desktop/workTools/demo/qxorm.db";const QString HOSTNAME="localhost";const QString USERNAME="root";const QString PASSWORD="";}class SqlHelper{public: static SqlHelper * getInstance(); bool init() bool isOldVersion(); //查询当前App 的数据库版本是否是以前的 如果是则调用updateDatabas void updateDatabase(); QSharedPointer<UserHandler> getUser();private: SqlHelper(){} SqlHelper(const SqlHelper&); SqlHelper& operator=(const SqlHelper); class CGarbo //单例自动回收 { public: CGarbo(){} ~CGarbo() if (SqlHelper::m_pSqlHelper) { delete SqlHelper::m_pSqlHelper; } } };private: static QMutex m_sMutex; static SqlHelper *m_pSqlHelper; static CGarbo m_sCGarbo;private: QSharedPointer<UserHandler> m_pUser; QSharedPointer<VersionHandler> m_pVersion;};#endif // SQLHELPER_HSqlHelper.cpp
    #include "SqlHelper.h"SqlHelper * SqlHelper ::m_pSqlHelper = nullptr;QMutex SqlHelper::m_sMutex;SqlHelper::CGarbo SqlHelper::m_sCGarbo;SqlHelper *SqlHelper::getInstance(){ SqlHelper* tmp = m_pSqlHelper; if (tmp == nullptr) { QMutexLocker lock(&m_sMutex); tmp = m_pSqlHelper; if (tmp == nullptr) { tmp = new SqlHelper(); m_pSqlHelper = tmp; } } return m_pSqlHelper;}bool SqlHelper::init(){ QSharedPointer<VersionHandler> tem1(new VersionHandler()); m_pVersion=tem1; QSharedPointer<UserHandler> tem2(new UserHandler()); m_pUser=tem2; return true;}bool SqlHelper::isOldVersion({ if(!m_pVersion.isNull()) { Shared_Version dbVersion(new VersionObject()); dbVersion->init(); m_pVersion->select(dbVersion); qDebug()<<"dbVersion->version"<<dbVersion->version; qDebug()<<" qApp->property()"<<qApp->property("DataBaseVersion").toInt(); qDebug()<<(dbVersion->version < qApp->property("DataBaseVersion").toInt()); return (dbVersion->version < qApp->property("DataBaseVersion").toInt()); } else { return false; }}void SqlHelper::updateDatabase(){ try { int dbversion=qApp->property("DataBaseVersion").toInt(); Shared_Version dbVersion(new VersionObject()); dbVersion->init(); m_pVersion->select(dbVersion); { QSqlDatabase db; if(QSqlDatabase::contains(SQLHELPER::CONNECT_NAME)) db = QSqlDatabase::database(SQLHELPER::CONNECT_NAME); else db= QSqlDatabase::addDatabase(SQLHELPER::DATABASE_TYPE,SQLHELPER::CONNECT_NAME); db.setDatabaseName(SQLHELPER::DATABASENAME); db.setHostName(SQLHELPER::HOSTNAME); db.setUserName(SQLHELPER::USERNAME); db.setPassword(SQLHELPER::PASSWORD); if(!db.isOpen()) { db.open(); } if (dbVersion->version >= dbversion) { if(db.isOpen()) { qDebug("**************** i come here ********************"); db.close(); } QSqlDatabase::removeDatabase(SQLHELPER::CONNECT_NAME); return; } QSqlQuery query(db); //获取在QxOrm注册的所有持久化类 qx::QxCollection<QString, qx::IxClass *> * pAllClasses = qx::QxClassX::getAllClasses(); if (! pAllClasses) { qAssert(false); return; } //将所有表获取到数据库中 QStringList tables = db.tables(); for (long k = 0; k < pAllClasses->count(); k++) { qx::IxClass * pClass = pAllClasses->getByIndex(k); if (! pClass) { continue; } // 过滤非persitents类 if (pClass->isKindOf("qx::service::IxParameter") || pClass->isKindOf("qx::service::IxService")) { continue; } // 筛选已经更新的类 if (pClass->getVersion() <= dbVersion->version) { continue; } qDebug()<<"****** pAllClasses->name ******"<<pClass->getName() <<pClass->getVersion(); // 如果表不存在,创建它并设置拥有者 if (! tables.contains(pClass->getName())) { qDebug()<<"***** want to creat table pClass->getName ******:"<<pClass->getName() query.exec("CREATE TABLE " + pClass->getName() + " ( ) WITH (OIDS = FALSE);" "ALTER TABLE " + pClass->getName() + " OWNER TO \"root\";"); //session += query.lastError(); } // 如果不存在列,则将其添加到表中 qx::IxDataMemberX * pDataMemberX = pClass->getDataMemberX(); for (long l = 0; (pDataMemberX && (l < pDataMemberX->count_WithDaoStrategy())); l++) { qx::IxDataMember * p = pDataMemberX->get_WithDaoStrategy(l); if (! p || (p->getVersion() <= dbVersion->version)){ continue; } qDebug()<<"***** add alter pClass->getName *****:"<<pClass->getName() <<p->getName()<<p->getSqlType(); query.exec("ALTER TABLE " + pClass->getName() + " ADD COLUMN " + p->getName() + " " + p->getSqlType() + ";"); //session += query.lastError(); if (p->getIsPrimaryKey()) // PRIMARY KEY { query.exec("ALTER TABLE " + pClass->getName() + " ADD PRIMARY KEY (" + p->getName() + ");"); //session += query.lastError(); } if (p->getAllPropertyBagKeys().contains("INDEX")) // INDEX { query.exec("CREATE INDEX " + pClass->getName() + "_" + p->getName() + "_idx" + " ON " + pClass->getName() + " USING " + p->getPropertyBag("INDEX").toString() + " (" + p->getName() + ");"); //session += query.lastError(); } if (p->getNotNull()) // NOT NULL { query.exec("ALTER TABLE " + pClass->getName() + " ALTER COLUMN " + p->getName() + " SET NOT NULL;"); //session += query.lastError(); } if (p->getAutoIncrement()) // AUTO INCREMENT { query.exec("CREATE SEQUENCE " + pClass->getName() + "_" + p->getName() + "_seq" + "; " "ALTER TABLE " + pClass->getName() + "_" + p->getName() + "_seq" + " OWNER TO \"root\"; " "ALTER TABLE " + pClass->getName() + " ALTER COLUMN " + p->getName() + " " + "SET DEFAULT nextval('" + pClass->getName() + "_" + p->getName() + "_seq" + "'::regclass);"); //session += query.lastError(); } if (p->getDescription() != "") // DESCRIPTION { query.exec("COMMENT ON COLUMN " + pClass->getName() + "." + p->getName() + " IS $$" + p->getDescription() + "$$ ;"); //session += query.lastError(); } } } //保存数据库的当前版本 dbVersion->version = dbversion; m_pVersion->update(dbVersion); if(db.isOpen()) { db.close(); } } QSqlDatabase::removeDatabase(SQLHELPER::CONNECT_NAME); } catch(const qx::dao::sql_error & err) { QSqlError sqlError = err.get(); qDebug() << sqlError.databaseText(); qDebug() << sqlError.driverText(); qDebug() << sqlError.number(); qDebug() << sqlError.type(); }}QSharedPointer<UserHandler> SqlHelper::getUser(){ return m_pUser;}是否有发现SQLHelper 里面有两个方法 isOldVersion() 和updateDatabase() 这个是用于校验当前app版本和数据库版本的方法,直接照抄就可以了修改官方并且验证过了。
    还有个common.h
    #ifndef COMMON_H#define COMMON_H#include <QString>const QString DATABASE_APP="DataBaseVersion";const int DATABASE_VERSION=0; //数据库版本控制#endif // COMMON_Hmain.cpp
    #include <QApplication>#include "common.h"int main(int argc, char *argv[]){ QApplication a(argc, argv); a.setProperty("DataBaseVersion",DATABASE_VERSION); return a.exec();}github源码:https://github.com/qq2690351079/ontheway 如果没有 就是我还没整理完还没上传
    后续会发出模板类的MVP 框架QT写的 不过改改C++也应该能直接用。
    数据库记得做DB 每次更新DB以后再update。
    0 留言 2019-07-23 14:39:54 奖励5点积分
  • windows下静态使用QxOrm框架并使用泛型编程 (二)

    这篇开始讲实际编程并且抽象化,让代码书写更少。
    为了模块划分我在Demo文件夹下新增了一个SQLModule文件夹,在此文件夹下又新增QxHandler,QxMapped,QxObject三个文件夹,QxObject是用来存储数据对象类的一个文件夹,QxMapped是存储键值映射的文件夹,QxHandler是操作数据库的实际句柄类文件夹。
    QxObject类
    User.h
    #ifndef USER_H#define USER_H#include "common.h"#include <QxOrm.h>#include <QJsonObject>class User{ QX_REGISTER_FRIEND_CLASS(User) //用于将类注册到QxOrm的宏定义public: User(); long getId(); QString getName(); int getAge();public: QJsonObject toJsonObject(); bool analyzeJson(QByteArray &json); void analyzeJson(QJsonObject &json);private: long m_lId QString m_sName; int m_nAge;};//QX_REGISTER_PRIMARY_KEY(User, int) //主键不是整数类型的时候使用QX_REGISTER_HPP_IMPORT_DLL(User, qx::trait::no_base_class_defined, DATABASE_VERSION) //用于将类注册到QxOrm的宏定义 第一个参数为类名 第二个为默认值 第三个为数据库版本#endif // USER_HUser.cpp
    #include "User.h"QX_REGISTER_CPP_IMPORT_DLL(User) //用于将类注册到QxOrm的宏定义namespace qx //用于将类注册到QxOrm的方法 { template <> void register_class(QxClass<User> & t) { qx::IxDataMember * pData = NULL; Q_UNUSED(pata); qx::IxSqlRelation * pRelation = NULL; Q_UNUSED(pRelation); qx::IxFunction * pFct = NULL; Q_UNUSED(pFct) qx::IxValidator * pValidator = NULL; Q_UNUSED(pValidator); // Register pData =t.id(& User::m_lId, "id",DATABASE_VERSION); pData =t.data(& User::m_nAge, "name",DATABASE_VERSION); pData =t.data(& User::m_nAge, "age",DATABASE_VERSION); qx::QxValidatorX<User> * pAllValidator = t.getAllValidator(); Q_UNUSED(pAllValidator); }}User::User() :m_lId(0) ,m_sName("") ,m_nAge(0){}long User::getId(){ return m_lId;}QString User::getName(){ return m_sName;}int User::getAge(){ return m_nAge}JsonObject User::toJsonObject(){ QJsonObject subObject; subObject.insert("Name",m_sName); subObject.insert("Age",m_nAge); return subObject;}bool User::analyzeJson(QByteArray &json){ bool success=false; QJsonParseError error; QJsonDocument document=QJsonDocument::fromJson(json,&error); if (!document.isNull() && (error.error == QJsonParseError::NoError)) { QJsonObject rootObject =documnt.object(); this->analyzeJson(rootObject); success=true; } else { success=false; qDebug()<<error.error; qDebug("LockoutFun analyze falied"); } return success;}void User::analyzeJson(QJsonObject &json){ QJsonObject rootObject = json.value("User").toObject(); m_sName=rootObject.value(QString("Name")).toString(); m_nAge=rootObject.value(QString("Age")).toInt();}QxMapped
    IMapped 映射抽象类
    #ifndef IMAPPED_H#define IMAPPED_H/*** @author tianmin* @brief The IMapped class 抽象出来的映射类* @date 2019-07-22*/#include <QStringList>#include <QSting>#include <QMapclass IMapped{public //字符map初始化 virtual void initMapped() = 0; //获取QString列表 virtual QStringList getListMapped(int en) =0; //获取QString 字段 virtual QString getMapped(int en) = 0; virtual ~IMapped(){}};#endif // IMAPPED_HUserMapped 际使用的类
    UserMapped .h
    #ifndef USERMAPPED_H#define USERMAPPED_H#include "IMapped.h"namespace USER {enum USER_MAPPED{ EN_ID = 0x00000001, EN_NAME = 0x00000002, EN_AGE = 0x00000004, EN_ALL = 0xFFFFFFFF};}class UserMapped:public IMapped{public: UserMapped(); virtual ~UserMapped();public: virtual void initMapped() virtual QStringList getListMapped(int en); virtual QString getMapped(int en);private: QMap<int,QString> m_map;};#endif // USERMAPPED_HUserMapped.cpp
    #include "UserMapped.h"UserMapped::UserMapped(){}UserMapped::~UserMapped(){}void UserMapped::initMapped(){ m_map.clear(); m_map.insert(USER::EN_ID, "id"); m_map.insert(USER::EN_NAME ,"name"); m_map.insert(USER::EN_AGE ,"age");}QStringList UserMapped::getListMapped(int en){ QStringList temp; QString str; QMap<int,QString>::iterator i; or (i = m_map.begin(); i != m_map.end(); ++i) { if(en&(i.key())) { str=i.value(); temp.append(str); str.clear(); } } return temp;}QString UserMapped::getMapped(int en){ QString str QMap<int,QString>::iterator i; for (i = m_map.begin(); i != m_map.end(); ++i) { if(en==i.key()) { str =i.value(); return str; } } return QString();}QxHandler
    IHandler 抽象化的类模板
    这里只抽象了部分方法 还有save 和事务以及关系的方法未抽象完成
    #ifndef IHANDLER_H#define IHANDLER_H#include <QxOrm.h>#include <QMutexLocker>#include <QMutex>#include <QtSql/QSqlDatabase>#include <QtSql/QSqlError>/*** @brief The ISqlInterface class 数据库操作的抽象方法类 并且模板化减少代码的繁杂*/class ISqlInterface{public: ISqlInterface(){} virtual ~ISqlInterface(){} //数据库连接初始化protected: virtual void initSqlconnect()=0; //建表 virtual bool createTable()=0; //断开连接 virtual void disconnect()=0;};template<class T,class T2,class T3>class IHandler{public: IHandler(){} virtual ~IHandler(){} Virtual bool createTable(QSqlDatabase &m_SqlDatabase) { QSqlError error= qx::dao::create_table<T3>(&m_SqlDatabase); return !error.isValid(); } virtual bool insert(T &t,QMutex &m_Mutex,QSqlDatabase &m_SqlDatabase) { QMutexLocker locker(&m_Mutex); if(!m_SqlDatabase.isOpen()) return false; if(qx::dao::exist(t,&m_SqlDatabase).getValue()!=false) return false; QSqlError error= qx::dao::insert(t,&m_SqlDatabase); return !error.isValid(); } virtual bool deleteObject(T &t,QMutex &m_Mutex,QSqlDatabase &m_SqlDatabase,bool isDestroy) { QMutexLocker locker(&m_Mutex); if(!m_SqlDatabase.isOpen()) return false; QSqlError error; if(qx::dao::exist(t,&m_SqlDatabase).getValue()==false) return false; if(isDestroy==false) { error= qx::dao::delete_by_id(t,&m_SqlDatabase); } else { error= qx::dao::destroy_by_id(t,&m_SqlDatabase); } return !error.isValid(); } virtual bool update(T &t,QMutex &m_Mutex,QSqlDatabase &m_SqlDatabase,QStringList &list) { QMutexLocker locker(&m_Mutex); if(!m_SqlDatabas.isOpen()) return false; if(qx::dao::exist(t,&m_SqlDatabase).getValue()==false) return false; QSqlError error= qx::dao::update(t,&m_SqlDatabase,list); return !error.isValid(); } virtual bool select(T &t,QMutex &m_Mutex,QSqlDatabase &m_SqlDatabase,QStringList &list) QMutexLocker locker(&m_Mutex); if(!m_SqlDatabase.isOpen()) return false; QSqlError error; if(qx::dao::exist(t,&m_SqlDatabase).getValue()==false) return false; error= qx::dao::fetch_by_id(t,&m_SqlDatabase,list); return !error.isValid(); } virtual bool selectByQuery(T2 &t,QMutex &m_Mutex,QSqlDatabase &m_SqlDatabase,qx::QxSqlQuery &query,QStringList &list) { QMutexLocker locker(&m_Mutx); if(!m_SqlDatabase.isOpen()) return false; QSqlError error=qx::dao::fetch_by_query(query,t,&m_SqlDatabase,list); return !error.isValid(); }};#endif // IHANDLER_HUserHandler 实际操作句柄类
    UserHandler.h
    #ifndef USERHANDLER_H#define USERHANDLER_H#include <QxOrm.h>#include <QString>#include <QMutexLocker>#include <QMutex>#include <QtSql/QSqlDatabase>#include <QtSql/QSqlError>#include "IHandler.h"#include "SQLModule/QxMapped/UserMapped.h"#include "SQLModule/QxObject/User.h"namespace USER{const QString DATABASE_TYPE="QSQLITE";const QString CONNECT_NAME="USER_CONNECTED";const QString DATABASENAME="C:/Users/we/Desktop/workTools/demo/qxorm.db";const QString HOSTNAME="localhost";const QString USERNAME="root";const QString PASSWORD="";}using namespace USER;class User; //OBJECT 类typedef QSharedPointer<User> Shared_User; //User类智能指针typedef QList<Shared_User> List_User; //User类数组typedef qx::QxCollection<int,Shared_User> Collection_User; //User容器class UserHandler:public IHandler<Shared_User,Collection_User,User> ,public ISqlInterface{public: UserHandler(); virtual ~UserHandler(); /** * @brief insert 插入数据至数据库 * @param t 插入的单条数据 不需要指定ID值 自增 * @return 0失败 1成功 */ bool insert(Shared_User &t); /** * @brief deleteObject 从数据库中删除指定数据 * @param t 删除的单条数据 需要指定ID值 * @param isDestroy 是否软删除 * @return 0失败 1成功 */ bool deleteObject(Shared_User &t,bool isDestroy=false); /** * @brief update 根据ID值更新数据 * @param t 数据 * @param en 更新字段的映射值 * @return 0失败 1成功 */ bool update(Shared_User &t,int en=EN_ALL); /** * @brief select 根据ID值查询数据 * @param t 数据 * @param en 映射值 * @return 0失败 1成功 */ bool select(Shared_User &t,int en=EN_ALL); /** * @brief selectByQuery 根据搜寻条件查找 * @param t 数据集合 * @param query 搜寻语句 * @param en 数据库列映射值 * @return 0失败 1成功 */ bool selectByQuery(Collection_User &t,qx::QxSqlQuery &query,int en=EN_ALL);protected: virtual void initSqlconnect(); virtual bool createTable(); virtual void disconnect();private: UserMapped m_Mapped; QMutex m_Mutex; QSqlDatabase m_SqlDatabase;};#endif // USERHANDLER_HUserHandler.cpp
    #include "UserHandler.h"UserHandler::UserHandler(){ initSqlconnect(); createTable();}UserHandler::~UserHandler(){ disconnect();}void UserHandler::initSqlconnect(){ QMutexLocker locker(&m_Mutex); if(QSqlDatabase::contains(CONNECT_NAME)) m_SqlDatabase = QSqlDatabase::database(CONNECT_NAME); else m_SqlDatabase= QSqlDatabase::addDatabase(DATABASE_TYPE,CONNECT_NAME); m_SqlDatabase.setDatabaseName(DATABASENAME); m_SqlDatabase.setHostName(HOSTNAME); m_SqlDatabase.setUserName(USERNAME); m_SqlDatabase.setPassword(PASSWORD); m_SqlDatabase.open();}bool UserHandler::createTable(){ return IHandler<Shared_User,Collection_User,User>::createTable(m_SqlDatabase);}void UserHandler::disconnect(){ QMutexLocker locker(&m_Mutex); if(m_SqlDatabase.isOpen()) m_SqlDatabase.close(); QSqlDatabase::removeDatabase(CONNECT_NAME);}bool UserHandler::insert(Shared_User &t){ return IHandler<Shared_User,Collection_User,User>::insert(t,m_Mutex,m_SqlDatabase);}bool UserHandler::deleteObject(Shared_User &t,bool isDestroy){ return IHandler<Shared_User,Collection_User,User>::deleteObject(t,m_Mutex,m_SqlDatabase,isDestroy);}bool UserHandler::update(Shared_User &t, int en){ QStringList list= m_Mapped.getListMapped(en); return IHandler<Shared_User,Collection_User,User>::update(t,m_Mutex,m_SqlDatabase,list);}bool UserHandler::select(Shared_User &t, int en){ QStringList list= m_Mapped.getListMapped(en); return IHandler<Shared_User,Collection_User,User>::select(t,m_Mutex,m_SqlDatabase,list);}bool UserHandler::selectByQuery(Collection_User &t,qx::QxSqlQuery &query,int en){ QStringList list= m_Mapped.getListMapped(en); return IHandler<Shared_User,Collection_User,User>::selectByQuery(t,m_Mutex,m_SqlDatabase,query,list);}
    0 留言 2019-07-23 14:38:57 奖励6点积分
  • windows下静态使用QxOrm框架并使用泛型编程 (一)

    开发环境
    Qt 5.6 for Desktop
    QxOrm版本为1.46 下载链接https://github.com/QxOrm/QxOrm

    下载完成以后QxOrm文件夹里会有简单的说明文档doc/index.html 不过有些方法是比较过时的,于是我测试了许久才测试好的框架代码。
    下载完成后打开QtCreator,选择新建项目选择 其他项目/子目录项目 类似下图的建立

    然后进入该项目的文件夹路径即200-300.pro所在路径 将QxOrm整个文件夹复制进来
    接着在200-300.pro文件中改为
    TEMPLATE = subdirsSUBDIRS += \ QxOrm这样就可以看到QxOrm项目成功加入到整体项目中了。而程序需要主项目作为运行所以我们右键200-300新建一个Application/Qt Widgets Application 我这边就叫Demo好了 。接着右键Demo添加库/外部库 库文件选择QxOrm/lib/libQxOrm.a 链接选择静态然后点下一步完成
    注意:需要在Demo.pro文件中加入QT += sql 这句话是将数据库加入到项目中去。
    目前环境就算是搭好了。下一篇继续讲如何使用。
    0 留言 2019-07-23 14:37:42 奖励8点积分
  • 【Cocos Creator 联机实战教程(2)】——匹配系统 精华

    1.知识点讲解大型多人互动游戏一般都会有一个匹配系统,用来让玩家进行联网游戏,现在我们来讲一讲这种系统吧,我们可以做个比较简单的双人对战匹配系统。
    我们让每一对匹配成功的玩家进入一个独立的房间,所以不同的房间的通信应该互不影响,由于不同场景的通信内容不同,所以不同场景的通信也应该独立
    我们把这个游戏的匹配过程比作开房的过程,

    如果有一个人进入了宾馆,那么他最先进入的区域就是hall(大厅),当然他可能就是逛逛,又推门出去
    当他想休息时他就去前台开个房,那么他就进入了queue(队列),并断开hall的通信
    当另一个人也想休息的时候也去前台排队,当个queue里有两个人的时候,前台小姐就给了他俩一个空闲房间的钥匙,他们就一起进入了一个独立的room,并断开queue的通信
    以上循环,房间数有限,在房间满的时候不能匹配成功

    当然,你也可以根据实际情况升级这个匹配系统,比如,分等级的匹配(开不同的队列等待)。
    注意:房卡游戏虽然也用到了房间这个概念,但不是匹配,这种游戏更像唱卡拉OK。进入大厅后,组织者去开个房间,其他人一起进。或者迟到的人拿着房间号直接进去。
    2. 步骤我们的游戏分为三个场景

    游戏启动的时候进入menu场景,当玩家点击对战时进入match场景,匹配成功进入game场景,取消匹配返回menu场景,游戏结束返回menu场景
    我们在Global里定义socket
    window.G = { globalSocket:null,//全局 hallSocket:null,//大厅 queueSocket:null,//队列 roomSocket:null,//房间 gameManager:null, chessManager:null, stand:null,}
    menu场景启动时,我们连接hallSocket,开始匹配时,断开hallSocket
    cc.Class({ extends: cc.Component, onLoad: function () { G.globalSocket = io.connect('127.0.0.1:4747'); //断开连接后再重新连接需要加上{'force new connection': true} G.hallSocket = io.connect('127.0.0.1:4747/hall',{'force new connection': true}); }, onBtnStart() { G.hallSocket.disconnect(); cc.director.loadScene('match'); }});
    进入match场景,连接queueSocket,先进入queue的玩家主场黑棋先手,后进入客场白棋后手(这个逻辑是服务端判断的),匹配成功时,服务端会发送roomId,玩家进入相应的房间,并断开queueSocket的通信
    const Constants = require('Constants');const STAND = Constants.STAND;cc.Class({ extends: cc.Component, onLoad: function () { G.queueSocket = io.connect('127.0.0.1:4747/queue', { 'force new connection': true }); G.queueSocket.on('set stand', function (stand) { if (stand === 'black') { G.stand = STAND.BLACK; } else if (stand === 'white') { G.stand = STAND.WHITE; } }); G.queueSocket.on('match success', function (roomId) { cc.log('match success' + roomId); G.roomSocket = io.connect('127.0.0.1:4747/rooms' + roomId, { 'force new connection': true }); G.queueSocket.disconnect(); cc.director.loadScene('game'); }); }, onBtnCancel() { G.queueSocket.disconnect(); cc.director.loadScene('menu'); }});
    在game场景中,如果游戏结束我们就断掉roomSocket回到menu场景
    startGame() { this.turn = STAND.BLACK; this.gameState = GAME_STATE.PLAYING; this.showInfo('start game'); },endGame() { let onFinished = () =>{ G.roomSocket.disconnect(); cc.director.loadScene('menu'); } this.infoAnimation.on('finished',onFinished,this); this.gameState = GAME_STATE.OVER; this.showInfo('game over'); },
    服务端完整逻辑
    let app = require('express')();let server = require('http').Server(app);let io = require('socket.io')(server);server.listen(4747, function() { console.log('listening on:4747');});let MAX = 30;//最大支持连接房间数let hall = null;//大厅let queue = null;//匹配队列let rooms = [];//游戏房间function Hall() { this.people = 0; this.socket = null;}function Room(){ this.people = 0; this.socket = null;}function Queue(){ this.people = 0; this.socket = null;}hall = new Hall();queue = new Queue();for(let n = 0;n < MAX;n++){ rooms[n] = new Room();}function getFreeRoom(){ for(let n = 0;n < MAX;n++){ if(rooms[n].people === 0){ return n; } } return -1;}io.people = 0;io.on('connection',function(socket){ io.people++; console.log('someone connected'); socket.on('disconnect',function(){ io.people--; console.log('someone disconnected'); });})hall.socket = io.of('/hall').on('connection', function(socket) { hall.people++; console.log('a player connected.There are '+hall.people+' people in hall'); hall.socket.emit('people changed',hall.people); socket.on('disconnect',function(){ hall.people--; console.log('a player disconnected.There are '+hall.people+' people in hall'); hall.socket.emit('people changed',hall.people); });});queue.socket = io.of('/queue').on('connection',function(socket){ queue.people++; console.log('someone connect queue socket.There are '+queue.people+' people in queue'); if(queue.people === 1){ socket.emit('set stand','black'); }else if(queue.people === 2){ socket.emit('set stand','white'); let roomId = getFreeRoom(); console.log(roomId+"roomId"); if(roomId >= 0){ queue.socket.emit('match success',roomId); console.log('match success.There are '+queue.people+' people in queue'); }else{ console.log('no free room!'); } } socket.on('cancel match',function(){ queue.people--; console.log('someone cancel match.There are '+queue.people+' people in queue'); }); socket.on('disconnect',function(){ queue.people--; console.log('someone disconnected match.There are '+queue.people+' people in queue'); });});for(let i = 0;i < MAX;i++){ rooms[i].socket = io.of('/rooms'+i).on('connection',function(socket){ rooms[i].people++; console.log('some one connected room'+i+'.There are '+rooms[i].people+' people in the room'); socket.on('update chessboard',function(chessCoor){ socket.broadcast.emit('update chessboard',chessCoor); }); socket.on('force change turn',function(){ socket.broadcast.emit('force change turn'); }); socket.on('disconnect',function(){ rooms[i].people--; console.log('someone disconnected room'+i+'.There are '+rooms[i].people+' people in the room'); }); });}
    3. 总结我们做的是比较简单的匹配系统,实际上还有匹配算法(选择排队的顺序不仅仅是先来后到)。
    这是我们需要掌握的新知识,除此之外我们都可以使用之前的知识点完成游戏。
    注意以下问题:

    跨场景访问变量
    在util下面有两个脚本,Constants用来存储游戏常量,然后其他地方需要常量时
    const Constants = require('Constants');const GAME_STATE = Constants.GAME_STATE;const STAND = Constants.STAND;const CHESS_TYPE = Constants.CHESS_TYPE;
    Global存储全局控制句柄,需要访问他们的时候,就可以通过(G.)的方式

    控制单位应该是脚本而不是节点
    本教程部分素材来源于网络。
    3 留言 2018-12-07 14:58:43 奖励35点积分
  • 中山大学智慧健康服务平台应用开发-基本的UI界面设计

    一、实验题目实验一: 中山大学智慧健康服务平台应用开发
    二、实现内容2.1 基本的UI界面设计实现一个Android应用,界面呈现如图中的效果。

    2.1.1 要求
    该界面为应用启动后看到的第一个界面
    各控件的要求

    标题字体大小20sp,与顶部距离20dp,居中图片与上下控件的间距均为20dp,居中输入框整体距左右屏幕各间距20dp,内容(包括提示内容)如图所示,内容字体大小18sp按钮与输入框间距10dp,文字大小18sp。按钮背景框左右边框与文字间距10dp,上下边框与文字间距5dp,圆角半径180dp,背景色为#3F51B5四个单选按钮整体居中,与输入框间距10dp,字体大小18sp,各个单选按钮之间间距10dp,默认选中的按钮为第一个

    2.1.2 使用的组件TextView、EditText、ConstraintLayout、Button、ImageView、RadioGroup、RadioButton。实现一个Android应用,界面呈现如图中的效果。
    2.1.3 验收内容
    各控件的位置,间距,字体大小等属性与要求无误
    图片大小不作为验收内容给之一

    2.2 事件处理
    2.2.1 要求
    该界面为应用启动后看到的第一个界面
    各控件处理的要求

    点击搜索按钮
    如果搜索内容为空,弹出Toast信息“搜索内容不能为空”如果搜索内容为“Health”,根据选中的RadioButton项弹出如下对话框点击“确定”,弹出Toast信息——对话框“确定”按钮被点击点击“取消”,弹出Toast 信息——对话框“取消”按钮被点击否则弹出如下对话框,对话框点击效果同上

    RadioButton选择项切换:选择项切换之后,弹出Toast信息“XX被选中”,例如从图片切换到视频,弹出Toast信息“视频被选中”

    2.2.2 验收内容
    布局是否正常
    搜索内容为空时,提示是否正常
    输入搜索内容后,点击搜索按钮是否能根据不同的搜索内容显示相应的弹出框,以及弹出框内容是否符合要求
    点击弹出框的相应按钮是否能提示正确的内容
    RadioButton切换时,提示是否正常

    2.3 Intent、Bundle的使用以及RecyclerView、ListView的应用本次实验模拟实现一个健康食品列表,有两个界面,第一个界面用于呈现食品列表如下所示。

    数据在”manual/素材”目录下给出。
    点击右下方的悬浮按钮可以切换到收藏夹。

    上面两个列表点击任意一项后,可以看到详细的信息:

    2.3.1 UI要求食品列表
    每一项为一个圆圈和一个名字,圆圈和名字都是垂直居中。圆圈内的内容是该食品的种类,内容要处于圆圈的中心,颜色为白色。食品名字为黑色,圆圈颜色自定义,只需能看见圆圈内的内容即可。
    收藏夹
    与食品列表相似。
    食品详情界面
    界面顶部

    顶部占整个界面的1/3。每个食品详情的顶部颜色在数据中已给出。返回图标处于这块区域的左上角,食品名字处于左下角,星标处于右下角,边距可以自己设置。 返回图标与名字左对齐,名字与星标底边对齐。 建议用RelativeLayout实现,以熟悉RelativeLayout的使用。
    界面中部

    使用的黑色argb编码值为#D5000000,稍微偏灰色的“富含”“蛋白质”的argb编码值为#8A000000。”更多资料”一栏上方有一条分割线,argb编码值为#1E000000。右边收藏符号的左边也有一条分割线,要求与收藏符号高度一致,垂直居中。字体大小自定。”更多资料”下方分割线高度自定。这部分所有的分割线argb编码值都是#1E000000。
    界面底部

    使用的黑色argb编码值为#D5000000。
    标题栏
    两个界面的标题栏都需要去掉。
    2.3.2 功能要求使用RecyclerView实现食品列表。点击某个食品会跳转到该食品的详情界面,呈现该食品的详细信息。长按列表中某个食品会删除该食品,并弹出Toast,提示 “删除XX” 。
    点击右下方的FloatingActionButton,从食品列表切换到收藏夹或从收藏夹切换到食品列表,并且该按钮的图片作出相应改变。
    使用ListView实现收藏夹。点击收藏夹的某个食品会跳转到食品详情界面,呈现该食品的详细信息。长按收藏夹中的某个食品会弹出对话框询问是否移出该食品,点击确定则移除该食品,点击取消则对话框消失。
    商品详情界面中点击返回图标会返回上一层。点击星标会切换状态,如果原本是空心星星,则会变成实心星星;原本是实心星星,则会变成空心星星。点击收藏图表则将该食品添加到收藏夹并弹出Toast提示 “已收藏” 。
    三、实验结果3.1 基本的UI界面设计与基础事件处理3.1.1 实验截图切换按钮时候,显示当前切换到的按钮名字,如下图,视频被选中:

    搜索Health关键词时,显示对话框搜索成功:

    搜索其他关键词,无法正确搜索,显示搜索错误对话框:

    点击取消按钮时,显示toast取消被单击:

    3.1.2 实验步骤以及关键代码这个实验前两部分包括简单的UI设计以及UI的交互。
    首先,我们当然要从UI的构建开始。
    1.插入标题以及图片这里应用到了TextView以及ImageView两个控件。由于本次的ui是使用ConstraintLayout布局,所以必须对每一个控件设置左右上下分别对齐什么。故要利用app:layout_constraintLeft_toLeftOf 等属性,表示该组件的左边对齐于xx的左边,这里的textview就要与parent即整个页面的左边对齐,然后设置居中。宽度,大小就根据实验要求来设置,而id是用于后面的交互部分识别该控件用的。
    <TextView android:id="@+id/title" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/title" android:textSize="20sp" app:layout_constraintTop_toTopOf="parent" android:layout_marginTop="20dp" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" /> <ImageView android:id="@+id/image" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="20dp" android:src="@mipmap/sysu" app:layout_constraintBottom_toTopOf="@+id/search_content" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toBottomOf="@id/title" />
    2.插入搜索输入框以及搜索按钮对于输入框要使用EditText控件,对于按钮要使用Button控件。对于输入框的显示内容,预先在string文件中写入,然后直接在控件中调用即可。对于button还用到了style属性,表示直接引用style写好的按钮样式。而style里面又调用了其他文件中已经预设好的属性,例如color中颜色。
    <style name="search_button"> <item name="android:textColor">@color/white</item> <item name="android:background">@drawable/button</item> </style>
    <EditText android:id="@+id/search_content" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_marginLeft="20dp" android:layout_marginRight="10dp" android:layout_marginTop="20dp" android:gravity="center" android:hint="@string/search_content" android:textSize="18sp" app:layout_constraintBottom_toTopOf="@+id/radioGroup" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toLeftOf="@+id/but1" app:layout_constraintTop_toBottomOf="@id/image" /> <Button android:id="@+id/but1" style="@style/search_button" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginRight="20dp" android:layout_marginTop="20dp" android:text="@string/search_button" android:textSize="18sp" app:layout_constraintBottom_toTopOf="@+id/radioGroup" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toBottomOf="@id/image" />
    3. 插入选择按钮选择按钮组要使用RadioGroup与RadioButton相配合,在group中设置边距以及大小,对于每一个radiobutton使用其他设置好的样式属性,在第一个选择按钮中设置checked属性设置为true就会默认第一个按钮被选定。
    <RadioGroup android:id="@+id/radioGroup" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="10dp" android:orientation="horizontal" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toBottomOf="@id/search_content"> <RadioButton android:id="@+id/selection1" style="@style/MyRadioButton" android:layout_height="match_parent" android:checked="true" android:text="@string/selection1" /> <RadioButton android:id="@+id/selection2" style="@style/MyRadioButton" android:text="@string/selection2" /> <RadioButton android:id="@+id/selection3" style="@style/MyRadioButton" android:text="@string/selection3" /> <RadioButton android:id="@+id/selection4" style="@style/MyRadioButton" android:text="@string/selection4" /> </RadioGroup>
    这就基本完成了UI的界面设置,接下来要根据他们的id来设置一些函数实现实验要求,例如弹出对话框或者toast等等。
    4.获取搜索输入框的内容,以及点击搜索按钮显示提示这一步主要要调用findViewById这个函数来分别得到输入框以及按钮,给按钮设置监听函数setOnClickListener, 然后在里面对于输入框的内容searchContent.getText().toString()来进行判断,分别有三种情况,搜索内容为空,搜索内容为Health,搜索内容为其他。
    然后,关于对话框的显示要使用dialog,分别给它设置标题,中间内容以及按钮。而toast则要对于对话框的按钮来设置监听函数,当点击时候来Toast.makeText()显示一个具体的toast内容。
    Button button =(Button) findViewById(R.id.but1); final EditText searchContent = (EditText) findViewById(R.id.search_content); button.setOnClickListener(new View.OnClickListener(){ @Override public void onClick(View view){ //搜索为空情况 if(TextUtils.isEmpty(searchContent.getText().toString())){ //弹出 Toast Toast.makeText(MainActivity.this, "搜索内容不能为空",Toast.LENGTH_SHORT).show(); } //搜索成功情况 else if(searchContent.getText().toString().equals("Health")){ AlertDialog.Builder dialog = new AlertDialog.Builder(MainActivity.this); dialog.setTitle("提示"); RadioButton temp = findViewById(radioGroup.getCheckedRadioButtonId()); dialog.setMessage(temp.getText().toString()+"搜索成功"); dialog.setPositiveButton("确定", new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialogInterface, int i) { Toast.makeText(MainActivity.this, "对话框“确定”按钮被点击",Toast.LENGTH_SHORT).show(); } }); dialog.setNegativeButton("取消", new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialogInterface, int i) { Toast.makeText(MainActivity.this, "对话框“取消”按钮被点击",Toast.LENGTH_SHORT).show(); } }); dialog.show(); } //搜索失败情况 else{ AlertDialog.Builder dialog = new AlertDialog.Builder(MainActivity.this); dialog.setTitle("提示"); dialog.setMessage("搜索失败"); dialog.setPositiveButton("确定", new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialogInterface, int i) { Toast.makeText(MainActivity.this, "对话框“确定”按钮被点击",Toast.LENGTH_SHORT).show(); } }); dialog.setNegativeButton("取消", new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialogInterface, int i) { Toast.makeText(MainActivity.this, "对话框“取消”按钮被点击",Toast.LENGTH_SHORT).show(); } }); dialog.show(); } } });
    4.对于选择按钮组的切换与上面相同,先要通过id来找到相应的控件,然后对于radioGroup来设置选择改变的监听函数,当切换的时候会根据选择的不同按钮上的信息来生成一个toast。
    final RadioGroup radioGroup = findViewById(R.id.radioGroup); final RadioButton radioButton = findViewById(radioGroup.getCheckedRadioButtonId()); radioGroup.setOnCheckedChangeListener(new RadioGroup.OnCheckedChangeListener(){ @Override //选择变化时,弹出toast提示信息 public void onCheckedChanged(RadioGroup group, int checkedID){ String str = ""; RadioButton select1 = findViewById(R.id.selection1); RadioButton select2 = findViewById(R.id.selection2); RadioButton select3 = findViewById(R.id.selection3); RadioButton select4 = findViewById(R.id.selection4); if(select1.getId() == checkedID){ str = select1.getText().toString(); } else if(select2.getId() == checkedID){ str = select2.getText().toString(); } else if(select3.getId() == checkedID){ str = select3.getText().toString(); } else if(select4.getId() == checkedID){ str = select4.getText().toString(); } Toast.makeText(MainActivity.this, str + "被选中",Toast.LENGTH_SHORT).show(); } });
    3.1.3 实验遇到的困难以及解决思路1.关于UI部分的边距问题起初对于ConstraintLayout布局不熟悉,不理解为什么需要对于一个控件的左右边限制跟随另一个的左右边,单纯认为只需要改变margin即可完成布局。而实际情况时,根据布局出来的结果可以看到仅改变margin之后相对于父亲来改变距离,而不能完全地设置两个组件的相应距离。于是完成一个组件时候,对于下一个组件的上下左右边缘要根据相对应的组件来限制一下。
    而在修改UI的时候,多使用preview功能以及在xml下切换至design模式,可以清晰看出组件之间的边距关系,查看布局是否正确。

    2.如何让中间的搜索框以及搜索按钮以合适的大小安放在同一行?这个问题就是在ui部分一直困扰我的,由于搜索框与左边要有限制,在右边又要与搜索按钮有限制,而搜索框也要与右边有限制。这样设置 app:layout_constraintRight_toRightOf 等等需要十分注意。
    而且输入框的长度也要合适,当 android:layout_width=”wrap_parent” 时候仅显示了提示内容的长度。而 android:layout_width=”fill_parent” 时候又占满了整个显示屏,显然是不行。而选择固定长度则不符合我们安卓手机界面设计的原则,无法在各种机型中显示合理。
    经过查询多种资料,可以通过设置 android:layout_width=”0dp” 来使这个输入框自适应边距,因此问题迎刃而解。
    3.实现交互部分的api比较通用的找到控件的函数为findViewById,通过id来找到控件,这与我们设置的id就很关键了,必须要注意大小写以及名字的正确性。
    关于组件的监听函数,包括点击按钮,切换radiobutton等等,都要了解其中的参数,查看手册。
    3.2 Intent、Bundle的使用以及RecyclerView、ListView的应用3.2.1 实验截图下图为食物列表的展示,浮标图案为前往收藏夹:

    下图为收藏夹初始页面的展示,浮标图案为返回主页样式:

    下图为大豆食物的详情信息:

    下图为点击星星以及收藏按钮产生的事件截图:

    下图为收藏大豆事件后,收藏夹的信息截图:

    下图为长按大豆列表删除时的操作截图:

    下图为在食物列表长按食物删除的操作截图:

    3.2.2 实验步骤以及关键代码本次实验的内容有点多,要完成三个页面的设计以及不同活动之间的信息交互。
    1.完成从搜索页面跳转到FoodList页面由于上次的实验中完成了一个搜索的界面,我为了将两次实验连接到一起,因此在搜索页面搜索switch时候会跳转到食物列表页面(即本次实验内容)
    要记得在mainfest中注册该活动,否则会出现应用闪退的现象,下面的两个页面也是如此,不再详述。

    这里使用startActivity以及intent来实现页面的跳转。
    ...//切换至食物列表,第二周任务的衔接第一周任务else if(searchContent.getText().toString().equals("switch")){ Intent intent = new Intent(); intent.setClass(MainActivity.this, FoodList.class); startActivity(intent);}...
    2.存储食物数据为了保存这些食物数据,我新建了一个MyCollection类来存储,类函数包括构造函数以及各个参数的get,set函数,不必详述。
    public class MyCollection implements Serializable { private String name; //食物名字 private String content; //食物图标 private String type; //食物种类 private String material; //食物成分 private boolean is_collected; //是否被收藏 private boolean is_star; //是否被加星 public MyCollection(){ is_collected =false; } public MyCollection(String _name, String _content, String _type, String _material, boolean _is_star){ name = _name; content = _content; type =_type; material = _material; is_star = _is_star; is_collected = false; } ... //各种get,set函数
    3.利用RecycleView实现FoodList这一部分可以说是这次实验的难点,我用了一天的时间才能理解RecycleView的实现过程。一个RecycleView需要一个Adater以及一个Holder来实现,存储的数据利用Holder,而用户点击的事件则利用Adater.
    首先实现MyViewHolder类,它必须继承RecyclerView.ViewHolder。其中通过findViewById函数来查找列表的填充项,如果已经查找过了就从数组中直接拿出即可,这样可以加快应用的速度,优化性能。
    public class MyViewHolder extends RecyclerView.ViewHolder { private SparseArray<View> views; private View view; public MyViewHolder(View _view) { super(_view); view = _view; views = new SparseArray<View>(); } public <T extends View> T getView(int _viewId) { View _view = views.get(_viewId); if (_view == null) { //创建view _view = view.findViewById(_viewId); //将view存入views views.put(_viewId, _view); } return (T) _view; }}
    接着是MyRecyclerViewAdapter类,它必须继承RecyclerView.Adapter类,其中利用MyViewHolder来存储列表的数据。该类实现点击的功能,这里新建了item的点击监听器,包括单击以及长按两种操作。
    除此之外,它必须重构onCreateViewHolder,onBindViewHolder,getItemCount这三个函数。
    在onBindViewHolder中为item来重构点击事件,其中长按事件函数要返回false,不然会与单击事件同时触发
    public class MyRecyclerViewAdapter<T> extends RecyclerView.Adapter<MyViewHolder>{ private List<MyCollection> data; private Context context; private int layoutId; private OnItemClickListener onItemClickListener; public MyRecyclerViewAdapter(Context _context, int _layoutId, List<MyCollection> _data){ context = _context; layoutId = _layoutId; data = _data; } //点击事件的接口 public interface OnItemClickListener{ void onClick(int position); void onLongClick(int position); } public void setOnItemClickListener(OnItemClickListener _onItemClickListener) { this.onItemClickListener = _onItemClickListener; } //删除数据 public void deleteData(int position){ data.remove(position); } @Override public MyViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { MyViewHolder holder = new MyViewHolder(LayoutInflater.from(context).inflate(R.layout.item, parent, false)); return holder; } @Override public void onBindViewHolder(final MyViewHolder holder, int position) { //convert ((TextView)holder.getView(R.id.recipeName)).setText(data.get(position).getName()); ((TextView)holder.getView(R.id.img)).setText(data.get(position).getContent()); if (onItemClickListener != null) { //单击 holder.itemView.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { onItemClickListener.onClick(holder.getAdapterPosition()); } }); //长按 holder.itemView.setOnLongClickListener(new View.OnLongClickListener() { @Override public boolean onLongClick(View view) { onItemClickListener.onLongClick(holder.getAdapterPosition()); return false; } }); } } @Override public int getItemCount() { if(!data.isEmpty()) return data.size(); return 0; }}
    我们先在FoodList.xml布局文件中预先设置了recycleview,以及新建一个item.xml来初始化列表项,包括两个textView组件来存放食物的标志以及文字。
    在FoodList.java中来通过recycleview的id来找到该组件,然后来通过adapter来设置。首先要利用setLatoutManager函数类似ListView来设置layout。
    然后设置监听器,单击跳转到详情页面根据点击的位置gotoDetail_for_Foodlist(position);该函数在后面部分叙述,此处只需知道它跳转到了详情页面。
    而长按时,要将数据删除,这里使用notifyItemRemoved(position);以及之前在Adapter实现了的删除函数来实现这一功能,最后弹出一个toast。
    RecyclerView recyclerView = findViewById(R.id.recyclerView); recyclerView.setLayoutManager(new LinearLayoutManager(FoodList.this)); // 类似ListView final MyRecyclerViewAdapter myAdapter = new MyRecyclerViewAdapter<MyCollection>(FoodList.this, R.layout.item, data2); myAdapter.setOnItemClickListener(new MyRecyclerViewAdapter.OnItemClickListener() { @Override public void onClick(int position) { gotoDetail_for_Foodlist(position); } @Override public void onLongClick(int position) { myAdapter.notifyItemRemoved(position); myAdapter.deleteData(position); Toast.makeText(FoodList.this,"移除第"+(position+1)+"个商品", Toast.LENGTH_SHORT).show(); } }); recyclerView.setAdapter(myAdapter); //不使用动画情况,后面为其加自定义动画,见实验思考内容
    到这里,我就完成了第一个页面FoodList中列表的设计,但是还需要一个浮动按键。根据实验的教程来引入依赖后,在FoodList.xml为其新建组件,设定id。
    然后在食物列表页面通过id找到按键来处理,这里要求改变图片以及展示的内容,需要用到setVisibility,setImageResource这两个函数,通过一个tag来确定显示哪个页面,然后通过设置其是否展示或者展示哪张图片即可。
    //点击浮标事件 final FloatingActionButton f_but = findViewById(R.id.btn); f_but.setOnClickListener(new View.OnClickListener(){ boolean tag_for_foodlist = true; @Override public void onClick(View v){ if(tag_for_foodlist){ findViewById(R.id.recyclerView).setVisibility(View.GONE);//设置Foodlist不可见 findViewById(R.id.listView).setVisibility(View.VISIBLE); tag_for_foodlist = false; f_but.setImageResource(R.mipmap.mainpage); } else{ findViewById(R.id.recyclerView).setVisibility(View.VISIBLE); findViewById(R.id.listView).setVisibility(View.GONE);//设置Favourite不可见 tag_for_foodlist = true; f_but.setImageResource(R.mipmap.collect); } } });
    4.利用ListView实现Collection收藏夹页面首先在FoodList建立listview组件,然后才能通过id来找到。
    ListView就比前面简单很多,可以直接使用simpleAdapter来直接设置,只需调整传入的内容参数即可,点击的监听器要分别设单击事件,前往详情页面;长按事件,弹出询问框是否删除,这一部分是上一实验的内容不再详述。
    //ListView部分 ListView listview = (ListView) findViewById(R.id.listView); simpleAdapter = new SimpleAdapter(this, favourite, R.layout.item, new String[] {"img", "recipeName"}, new int[] {R.id.img, R.id.recipeName}); listview.setAdapter(simpleAdapter); listview.setOnItemClickListener(new AdapterView.OnItemClickListener() { @Override public void onItemClick(AdapterView<?> adapterView, View view, int i, long l) { if(i != 0) gotoDetail_for_Collect(i); } }); listview.setOnItemLongClickListener(new AdapterView.OnItemLongClickListener() { @Override public boolean onItemLongClick(AdapterView<?> adapterView, View view, int i, long l) { // 处理长按事件 if(i != 0){ //弹出询问框 final int delete_num = i; AlertDialog.Builder dialog = new AlertDialog.Builder(FoodList.this); dialog.setTitle("删除"); dialog.setMessage("确定删除"+favourite.get(i).get("recipeName")+"?"); dialog.setPositiveButton("确定", new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialogInterface, int i) { favourite.remove(delete_num); simpleAdapter.notifyDataSetChanged(); } }); dialog.setNegativeButton("取消", new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialogInterface, int i) { } }); dialog.show(); } return true; //这样长按与短按不会同时触发 } });
    5.利用Relative布局,Linear布局,Constraint布局实现Detail详情页面的UI部分这一部分需要我了解各种布局的一些具体情况,比如如何设置水平垂直居中,如何设置三分之一,如何与别的组件保持在一水平线上等等。
    因为上次实验已经使用了Constraint布局来设计UI,所以这里只分析一下对于用Relative布局的详情页面的顶部,要将顶部设置为三分之一,需要利用android:layout_weight=”1”这一属性,需要注意的是使用这一属性时,必须将高度设置为0,让其自动来匹配页面,以达成三分之一的效果。
    对于RelativeLayout布局,layout_alignParentLeft表示返回图标位于页面的左侧,其次食物名字要与返回图标的左侧对齐就要使用android:layout_alignLeft=”@id/back”,里面的参数为想要对齐的id。
    对于星星图标的处理可以预先设置为空星星,而且增加tag来为后面的变化做准备。
    而我为了保存星星的状态,不使用这一方法,所以在xml上不写图片,而在Detail.xml根据食物来动态生成。
    <RelativeLayout android:id="@+id/top" android:layout_width="match_parent" android:layout_height="0dp" android:background="#3F51BB" android:layout_weight="1" > <ImageView android:id="@+id/back" android:layout_width="50dp" android:layout_height="50dp" android:src="@mipmap/back" android:layout_alignParentLeft="true" android:layout_marginTop="10dp" android:layout_marginStart="10dp" android:layout_marginLeft="10dp" /> <TextView android:id="@+id/name" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentBottom="true" android:layout_marginBottom="20dp" android:layout_marginRight="20dp" android:text="牛奶" android:textSize="30dp" android:textColor="@color/white" android:layout_alignLeft="@id/back" android:layout_marginEnd="10dp" /> <ImageView android:id="@+id/star" android:layout_width="40dp" android:layout_height="40dp" android:layout_alignParentEnd="true" android:layout_alignTop="@+id/name" android:layout_marginEnd="20dp" android:layout_marginRight="20dp" android:layout_alignParentRight="true" /> </RelativeLayout>
    6.利用Intent,startActivity等实现不同活动之间的传递信息从前面点击监听器所绑定的跳转函数开始说明,这里是跳转到详情食物页面的函数,它必须根据坐标来将MyCollection中的内容读取出来,并将其放到bundle中,利用startActivityForResult来跳转到详情页面并等待返回参数来进行处理。这里需要的处理包括星星事件的点击,已经加入收藏夹的事件。
    private void gotoDetail_for_Foodlist(int position){ Intent intent = new Intent(); intent.setClass(FoodList.this,Details.class); Bundle bundle = new Bundle(); String s[] = new String [5]; s[0] = data2.get(position).getName(); s[1] = data2.get(position).getMaterial(); s[2] = data2.get(position).getType(); s[3] = data2.get(position).getContent(); s[4] = data2.get(position).getIs_star()?"yes":"no"; bundle.putStringArray("msg",s); intent.putExtras(bundle); startActivityForResult(intent,REQUEST_CODE);//REQUEST_CODE --> 1 }
    然后,在Detail.java中返回参数到主页面的函数。当点击星星以及收藏时候,我们只改变MyCollection的属性,而不是真正返回活动,而到点击返回按钮时候才根据这些改变的属性来传递不同的参数。

    当返回2时,表示详情页面出现了收藏事件,必须将MyCollection的信息传递回去bundle.putSerializable(“collect”, temp),并且使用setResult来返回参数
    当返回3时,表示详情页面出现了改变星星状态事件
    当返回4时,表示两种事件同时发生

    不然,则直接调用finish事件来结束活动。
    //处理返回按钮 final ImageView back_but = findViewById(R.id.back); back_but.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { if(temp.getIs_collected() == true && temp.getIs_star() != (str[4].equals("yes"))){ Intent intent = new Intent(Details.this, FoodList.class); Bundle bundle = new Bundle(); bundle.putSerializable("collect", temp); intent.putExtras(bundle); setResult(4,intent); //RESULT_CODE --> 4 } //收藏夹 else if (temp.getIs_collected() == true) { Intent intent = new Intent(Details.this, FoodList.class); Bundle bundle = new Bundle(); bundle.putSerializable("collect", temp); intent.putExtras(bundle); setResult(2,intent); //RESULT_CODE --> 2 } //保存星星状态 else if(temp.getIs_star() != (str[4].equals("yes"))){ Intent intent = new Intent(Details.this, FoodList.class); Bundle bundle = new Bundle(); bundle.putSerializable("collect", temp); intent.putExtras(bundle); setResult(3,intent); //RESULT_CODE --> 3 } Details.this.finish(); } });
    这样在主页面中只需重构OnActivityResult函数即可以处理这些事件。处理结果为2时,从intent中拿回食物的信息,通知收藏夹列表来改变列表,这里使用我的私有函数refreshList,太过简单也不再细述,详情参见代码。
    而处理结果为3时,则要在两个列表中查找所有该食物的状态,更改星星的情况,以此实现星星状态的长期保存。
    // 为了获取结果 @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); if (resultCode == 2) { if (requestCode == REQUEST_CODE) { MyCollection mc = (MyCollection)data.getSerializableExtra("collect"); refreshList(mc,simpleAdapter); } } else if(resultCode == 3){ if (requestCode == REQUEST_CODE) { MyCollection mc = (MyCollection)data.getSerializableExtra("collect"); for(int i = 0; i < data2.size(); i++){ if(data2.get(i).getName().equals(mc.getName())){ data2.set(i,mc); } } for(int i = 0; i < favourite.size(); i++){ if(favourite.get(i).get("recipeName").toString().equals(mc.getName())){ favourite.get(i).remove("star"); favourite.get(i).put("star",mc.getIs_star()); } } } } else if(resultCode == 4){ if (requestCode == REQUEST_CODE) { MyCollection mc = (MyCollection)data.getSerializableExtra("collect"); for(int i = 0; i < data2.size(); i++){ if(data2.get(i).getName().equals(mc.getName())){ data2.set(i,mc); } } refreshList(mc,simpleAdapter); } } }
    7.在Detail页面来根据不同的食物内容来动态生成UI界面这里的动态包括,详情页面上部的颜色,以及名字,营养成分等等。星星的状态也要动态生成。
    通过intent拿出的信息来改变xml中相对应id的组件,只需注意id的正确,以及颜色的选取即可。
    Bundle bundle=this.getIntent().getExtras(); str = bundle.getStringArray("msg"); TextView name = findViewById(R.id.name); name.setText(str[0]); TextView material = findViewById(R.id.material); material.setText("富含 "+ str[1]); TextView type = findViewById(R.id.type); type.setText(str[2]); temp = new MyCollection(str[0],str[3],str[2],str[1],false); //根据上次情况保存星星状态 final ImageView star_but = findViewById(R.id.star); if(str[4].equals("yes")){ star_but.setImageResource(R.mipmap.full_star); temp.setIs_star(true); } else{ star_but.setImageResource(R.mipmap.empty_star); temp.setIs_star(false); }
    3.2.3 实验遇到的困难以及解决思路1.RecycleView无法正确生成列表按照老师给的教程一步步写好Adapter与Holder后,运行应用时出现闪退情况。报错信息为,无法得到资源。一有报错,第一步当然是将报错信息扔上搜索引擎,但是网页上的信息都说是因为setText()里面的参数为String而不是其他。但细看自己的程序并没有出现setText的错误参数情况。
    然后,我对于类的传参开始找问题,结果发现是convert函数在传参的时候,没有找到资源,而是一个空的对象。于是再修改convert函数后,完成了这一部分的工作。
    2.收藏列表的错误点击收藏列表的第一项为“*”与“收藏夹”,这两个不应该被触发点击事件,否则会传递一个空的MyCollection到详情页面会出现报错。所以必须在点击收藏列表的监听函数时加一个判断,当点击的是第一个item时,不要触发跳转事件。
    3.食物详情页面的UI设计不符合位置这次详情页面的UI有点难度,对于三分之一的上部设置就弄了相当长的时间,当知道使用layout_weight时候,然而在实际使用的时候,却并没有达到三分之一的效果。后来,才知道没有将height设置为0dp,而是为wrap_content.。这样导致权重设置失败。
    其次,对于设置分割线以及收藏图标如何垂直居中,间距合适遇到了困难。由于我在下部使用的是ConstrainLayout布局,所以必须要以别的组件来作相对设置位置。这里我对于这两个组件,分别相对于parent的上方,以及下面分割线的下方作为限制。这样就好像上下两个作用力,使其位于垂直居中的位置。
    最后只需调整线条的长度以及图片的大小即可。
    <TextView android:id="@+id/ver_line" android:layout_width="2dp" android:layout_height="45dp" android:background="#1E000000" app:layout_constraintTop_toTopOf="parent" app:layout_constraintBottom_toBottomOf="@id/line1" android:gravity="center" android:layout_marginRight="10dp" app:layout_constraintRight_toLeftOf="@+id/collect" /> <ImageView android:id="@+id/collect" android:layout_width="45dp" android:layout_height="45dp" android:scaleType="fitXY" android:src="@mipmap/collect" app:layout_constraintBottom_toBottomOf="@id/line1" app:layout_constraintTop_toTopOf="parent" app:layout_constraintRight_toRightOf="parent" android:layout_marginRight="20dp" />
    4.Detail页面与Foodlist页面进行交换信息时候,对于数据包的处理由于要使用intent来实现不同活动中的交互,必须将食物的信息传递到详情页面,以及在详情页面中改变后的食物信息传递回食物列表存储。于是,就要求他们交换信息时候必须要满足两个条件,第一是要一次传递一个食物对象,第二是要满足intent的信息交互函数。这里使用的是bundle的putSerializable函数,这也要求我们的食物类必须要实现Serializable类。
    Intent intent = new Intent(Details.this, FoodList.class); Bundle bundle = new Bundle(); bundle.putSerializable("collect", temp); intent.putExtras(bundle); setResult(2,intent); //RESULT_CODE --> 2
    四、实验思考及感想4.1 UI界面设计与基础事件交互本次是第一次安卓开发的实验,主要关于UI界面的设计与一些简单的交互,这与我之前学过的web网页设计十分相似,定义组件以及通过id来对于每一个组件来设置一些监听函数,完成所需要的功能。
    但是,安卓开发上也有许多不同之处,对于java文件中必须要了解调用组件的监听函数,名字比较长,而且参数多,必须在平时熟练使用并要经常查阅手册。
    对于ui界面,我这次主要是通过xml的书写来生成界面,用里面的一些属性来定义组件的大小,边距等等,除此之外,安卓开发中还很讲究文件的分类,将string,color,style设置成另外的文件,在主的xml可以调用这些文件中的内容来实现,这样的好处便于修改界面的内容,例如可以根据这个来开发中文英文不同的ui界面版本。
    4.2 Intent、Bundle的使用以及RecyclerView、ListView的应用这次实验花了不少的时间来理解不同列表的实现方式,学习了不同ui布局的位置设置,活动之间的交互信息方法,按钮监听函数。
    但是,对于实验基本要求所做出了的应用程序还是有一些不太完美的地方,于是,我做了一些改进的地方(加分项),使其更加符合日常使用,包括对于详情列表星星的状态保存,在详情页面不按返回图标而是点击手机的返回键时无法收藏该食物状态,还为RecycleList加了一个自定义的动画效果,使其更加美观。
    4.2.1 对于星星状态的持久化改进星星的状态持久化,我实现出来的效果是当该食物被加星后,无论是在食物列表还是在收藏列表都会出现加星的同步状态,不会出现个别加星个别不加星。
    这里实现的持久化,实际就是给食物添加多一个is_star属性来判断该食物的状态,并将该状态传递到详情页面来动态处理。
    //根据上次情况保存星星状态 final ImageView star_but = findViewById(R.id.star); if(str[4].equals("yes")){ star_but.setImageResource(R.mipmap.full_star); temp.setIs_star(true); } else{ star_but.setImageResource(R.mipmap.empty_star); temp.setIs_star(false); }
    而在改变后返回到其他界面时,也要将改变了的星星状态返回,以此改变该食物在数据结构中的信息
    else if(resultCode == 3){ if (requestCode == REQUEST_CODE) { MyCollection mc = (MyCollection)data.getSerializableExtra("collect"); for(int i = 0; i < data2.size(); i++){ //更新FoodList if(data2.get(i).getName().equals(mc.getName())){ data2.set(i,mc); } } for(int i = 0; i < favourite.size(); i++){ //更新CollectList if(favourite.get(i).get("recipeName").toString().equals(mc.getName())){ favourite.get(i).remove("star"); favourite.get(i).put("star",mc.getIs_star()); } } } }
    4.2.2 对于手机系统返回键的处理这里出现的bug是在详情页面点击收藏后,不按返回小图标,而是点击手机返回键时,无法收藏该食物。这是因为点击手机收藏键是没有将信息传递回主页面的,所以我们必须根据这个按键重构返回键的功能,来让该功能与点击返回小图标是一样的。
    当在详情页面,得到返回键被单击时,实现的功能与点击返回图标相同。而其他则继续执行系统的默认按键功能在最后添加return super.onKeyDown(keyCode, event);
    //点击返回时候,加入收藏也要生效 @Override public boolean onKeyDown(int keyCode, KeyEvent event) { if (keyCode == KeyEvent.KEYCODE_BACK) { //两种情况同时实现 if(temp.getIs_collected() == true && temp.getIs_star() != (str[4].equals("yes"))){ Intent intent = new Intent(Details.this, FoodList.class); Bundle bundle = new Bundle(); bundle.putSerializable("collect", temp); intent.putExtras(bundle); setResult(4,intent); //RESULT_CODE --> 4 } //收藏夹 else if (temp.getIs_collected() == true) { Intent intent = new Intent(Details.this, FoodList.class); Bundle bundle = new Bundle(); bundle.putSerializable("collect", temp); intent.putExtras(bundle); setResult(2,intent); //RESULT_CODE --> 2 } //保存星星状态 else if(temp.getIs_star() != (str[4].equals("yes"))){ Intent intent = new Intent(Details.this, FoodList.class); Bundle bundle = new Bundle(); bundle.putSerializable("collect", temp); intent.putExtras(bundle); setResult(3,intent); //RESULT_CODE --> 3 } else{ Details.this.finish(); } } return super.onKeyDown(keyCode, event); }
    4.2.3 实现RecycleList的动画在res文件夹建立anim文件夹来放置动画的xml文件,首先要建立layout_animation_fall_down.xml文件。
    其中animation为列表每一项item的动画,其文件在后面再实现,delay表示动画的延迟时间,animationOrder表示动画item的顺序是正常,即从大到小,在这里实现的效果就是从高到低。
    <?xml version="1.0" encoding="utf-8"?><layoutAnimation xmlns:android="http://schemas.android.com/apk/res/android" android:animation="@anim/item_animation_fall_down" android:delay="15%" android:animationOrder="normal" />
    接着对item实现layout_animation_fall_down.xml文件,来控制列表每一项的动画效果。
    translate组件中fromYDelta表示item首先位于y轴的上方20%出发,然后toYDelta表示item所要到达的位置,这里的0表示为回到本应该的位置。interpolator里面的属性表示减速实现动画过程。
    alpha组件表示透明度的变化,由0到1,加速实现动画过程。
    scale组件表示item的大小,由105%变化为100%,略微缩放动画。
    <set xmlns:android="http://schemas.android.com/apk/res/android" android:duration="500" > <translate android:fromYDelta="-20%" android:toYDelta="0" android:interpolator="@android:anim/decelerate_interpolator" /> <alpha android:fromAlpha="0" android:toAlpha="1" android:interpolator="@android:anim/decelerate_interpolator" /> <scale android:fromXScale="105%" android:fromYScale="105%" android:toXScale="100%" android:toYScale="100%" android:pivotX="50%" android:pivotY="50%" android:interpolator="@android:anim/decelerate_interpolator" /></set>
    这样就实现了一个列表的从上到下,逐渐出现的动画。
    4.3 感想通过不断的学习,总算理解了android的一些机制,也能简单的写出了一个程序了。但是对于java语言的虚函数,静态函数,接口,数据类型等等都需要加强,这会使我更方便地理解类与类之间的关系。对于ui的设计要熟练掌握三种布局的运用,可以适当给某些组件先赋值通过preview来查看位置,再在java文件中实现动态赋值,这样做既能保证ui也能动态生成页面。这次实验使用的是绑定数据是运用数组,猜想未来应该可以引入数据库的绑定,这样会使代码更加简洁。
    1 留言 2019-07-11 12:12:51 奖励20点积分
  • 【Cocos Creator 实战教程(1)】——人机对战五子棋(节点事件相关) 精华

    一、涉及知识点
    场景切换按钮事件监听节点事件监听节点数组循环中闭包的应用动态更换sprite图片定时器预制资源
    二、步骤2.1 准备工作首先,我们要新建一个空白工程,并在资源管理器中新建几个文件夹

    在这些文件夹中,我们用来存放不同的资源,其中

    Scene用来存放场景,我们可以把场景看作一个关卡,当关卡切换时,场景就切换了
    Script用来存放脚本文件
    Texture用来存放显示的资源,例如音频,图片
    Prefab用来存放预制资源,接下来我会详细的介绍

    接下来,我们在Scene文件夹中新建一个场景(右击文件夹->新建->Scene),命名为Menu,接着导入背景图片(直接拖拽即可)。最后调整图片大小使图片铺满背景,效果如图。

    2.2 按钮监听与场景切换接下来我们来学习此次实战的第一个难点,按钮监听与场景切换。
    首先,创建一个Button节点,并删除label,放在人机博弈的按钮上,并在属性上调成透明样式。

    接下来,新建一个Game场景,并添加一个棋盘节点,并把锚点设为(0,0)。

    这里,讲一下锚点的作用。
    anchor point 究竟是怎么回事? 之所以造成不容易理解的是因为我们平时看待一个图片是以图片的中心点这一个维度来决定图片的位置的。而在cocos2d中决定一个图片的位置是由两个维度一个是 position 另外一个是anchor point。只要我们搞清楚他们的关系,自然就迎刃而解。默认情况下,anchor point在图片的中心位置(0.5, 0.5),取值在0到1之间的好处就是,锚点不会和具体物体的大小耦合,也即不用关注物件大小,而应取其对应比率,如果把锚点改成(0,0),则进行放置位置时,以图片左下角作为起始点。也就是说,把position设置成(x,y)时,画到屏幕上需要知道:到底图片上的哪个点放在屏幕的(x,y)上,而anchor point就是这个放置的点,anchor point是可以超过图片边界的,比如下例中的(-1,-1),表示从超出图片左下角一个宽和一个高的地方放置到屏幕的(0,0)位置(向右上偏移10个点才开始到图片的左下角,可以认为向右上偏移了10个点的空白区域)
    他们的关系是这样的(假设actualPosition.x,actualPosition.y是真实图片位置的中点):actualPosition.x = position.x + width*(0.5 - anchor_point.x);acturalPosition.y = position.y + height*(0.5 - anchor_point.y)actualPosition 是sprite实际上在屏幕显示的位置, poistion是 程序设置的, achor_point也是程序设置的。
    然后,我们需要新建一个脚本,Menu.js,并添加开始游戏方法。
    cc.Class({ extends: cc.Component, startGame:function(){ cc.director.loadScene('Game');//这里便是运用导演类进行场景切换的代码 }});
    这里提示以下,编辑脚本是需要下载插件的,我选择了VScode,还是很好用的。
    最后我们将其添加为Menu场景的Canvas的组件(添加组件->脚本组件->menu),并在BtnP2C节点上添加按钮监听响应。


    这样,按钮监听就完成了。现在我们在Menu场景里点击一下人机按钮就会跳转到游戏场景了。
    2.3 预制资源预制资源一般是在场景里面创建独立的子界面或子窗口,即预制资源是存放在资源中,并不是节点中,例如本节课中的棋子。
    现在我们就来学习一下如何制作预制资源。

    再将black节点改名为Chess拖入下面Prefab文件夹使其成为预制资源。
    这其中,SpriteFrame 是核心渲染组件 Sprite 所使用的资源,设置或替换 Sprite 组件中的 spriteFrame 属性,就可以切换显示的图像,将其去掉防止图片被预加载,即棋子只是一个有大小的节点不显示图片也就没有颜色。
    2.4 结束场景直接在Game场景上制作结束场景。

    2.5 游戏脚本制作人机对战算法参考了这里https://blog.csdn.net/onezeros/article/details/5542379
    具体步骤便是初始化棋盘上225个棋子节点,并为每个节点添加事件,点击后动态显示棋子图片。
    代码如下:
    cc.Class({ extends: cc.Component, properties: { overSprite:{ default:null, type:cc.Sprite, }, overLabel:{ default:null, type:cc.Label }, chessPrefab:{//棋子的预制资源 default:null, type:cc.Prefab }, chessList:{//棋子节点的集合,用一维数组表示二维位置 default: [], type: [cc.node] }, whiteSpriteFrame:{//白棋的图片 default:null, type:cc.SpriteFrame }, blackSpriteFrame:{//黑棋的图片 default:null, type:cc.SpriteFrame }, touchChess:{//每一回合落下的棋子 default:null, type:cc.Node, visible:false//属性窗口不显示 }, gameState:'white', fiveGroup:[],//五元组 fiveGroupScore:[]//五元组分数 }, //重新开始 startGame(){ cc.director.loadScene("Game"); }, //返回菜单 toMenu(){ cc.director.loadScene("Menu"); }, onLoad: function () { this.overSprite.node.x = 10000;//让结束画面位于屏幕外 var self = this; //初始化棋盘上225个棋子节点,并为每个节点添加事件 for(var y = 0;y<15;y++){ for(var x = 0;x < 15;x++){ var newNode = cc.instantiate(this.chessPrefab);//复制Chess预制资源 this.node.addChild(newNode); newNode.setPosition(cc.p(x*40+20,y*40+20));//根据棋盘和棋子大小计算使每个棋子节点位于指定位置 newNode.tag = y*15+x;//根据每个节点的tag就可以算出其二维坐标 newNode.on(cc.Node.EventType.TOUCH_END,function(event){ self.touchChess = this; if(self.gameState === 'black' && this.getComponent(cc.Sprite).spriteFrame === null){ this.getComponent(cc.Sprite).spriteFrame = self.blackSpriteFrame;//下子后添加棋子图片使棋子显示 self.judgeOver(); if(self.gameState == 'white'){ self.scheduleOnce(function(){self.ai()},1);//延迟一秒电脑下棋 } } }); this.chessList.push(newNode); } } //开局白棋(电脑)在棋盘中央下一子 this.chessList[112].getComponent(cc.Sprite).spriteFrame = this.whiteSpriteFrame; this.gameState = 'black'; //添加五元数组 //横向 for(var y=0;y<15;y++){ for(var x=0;x<11;x++){ this.fiveGroup.push([y*15+x,y*15+x+1,y*15+x+2,y*15+x+3,y*15+x+4]); } } //纵向 for(var x=0;x<15;x++){ for(var y=0;y<11;y++){ this.fiveGroup.push([y*15+x,(y+1)*15+x,(y+2)*15+x,(y+3)*15+x,(y+4)*15+x]); } } //右上斜向 for(var b=-10;b<=10;b++){ for(var x=0;x<11;x++){ if(b+x<0||b+x>10){ continue; }else{ this.fiveGroup.push([(b+x)*15+x,(b+x+1)*15+x+1,(b+x+2)*15+x+2,(b+x+3)*15+x+3,(b+x+4)*15+x+4]); } } } //右下斜向 for(var b=4;b<=24;b++){ for(var y=0;y<11;y++){ if(b-y<4||b-y>14){ continue; }else{ this.fiveGroup.push([y*15+b-y,(y+1)*15+b-y-1,(y+2)*15+b-y-2,(y+3)*15+b-y-3,(y+4)*15+b-y-4]); } } } }, //电脑下棋逻辑 ai:function(){ //评分 for(var i=0;i<this.fiveGroup.length;i++){ var b=0;//五元组里黑棋的个数 var w=0;//五元组里白棋的个数 for(var j=0;j<5;j++){ this.getComponent(cc.Sprite).spriteFrame if(this.chessList[this.fiveGroup[i][j]].getComponent(cc.Sprite).spriteFrame == this.blackSpriteFrame){ b++; }else if(this.chessList[this.fiveGroup[i][j]].getComponent(cc.Sprite).spriteFrame == this.whiteSpriteFrame){ w++; } } if(b+w==0){ this.fiveGroupScore[i] = 7; }else if(b>0&&w>0){ this.fiveGroupScore[i] = 0; }else if(b==0&&w==1){ this.fiveGroupScore[i] = 35; }else if(b==0&&w==2){ this.fiveGroupScore[i] = 800; }else if(b==0&&w==3){ this.fiveGroupScore[i] = 15000; }else if(b==0&&w==4){ this.fiveGroupScore[i] = 800000; }else if(w==0&&b==1){ this.fiveGroupScore[i] = 15; }else if(w==0&&b==2){ this.fiveGroupScore[i] = 400; }else if(w==0&&b==3){ this.fiveGroupScore[i] = 1800; }else if(w==0&&b==4){ this.fiveGroupScore[i] = 100000; } } //找最高分的五元组 var hScore=0; var mPosition=0; for(var i=0;i<this.fiveGroupScore.length;i++){ if(this.fiveGroupScore[i]>hScore){ hScore = this.fiveGroupScore[i]; mPosition = (function(x){//js闭包 return x; })(i); } } //在最高分的五元组里找到最优下子位置 var flag1 = false;//无子 var flag2 = false;//有子 var nPosition = 0; for(var i=0;i<5;i++){ if(!flag1&&this.chessList[this.fiveGroup[mPosition][i]].getComponent(cc.Sprite).spriteFrame == null){ nPosition = (function(x){return x})(i); } if(!flag2&&this.chessList[this.fiveGroup[mPosition][i]].getComponent(cc.Sprite).spriteFrame != null){ flag1 = true; flag2 = true; } if(flag2&&this.chessList[this.fiveGroup[mPosition][i]].getComponent(cc.Sprite).spriteFrame == null){ nPosition = (function(x){return x})(i); break; } } //在最最优位置下子 this.chessList[this.fiveGroup[mPosition][nPosition]].getComponent(cc.Sprite).spriteFrame = this.whiteSpriteFrame; this.touchChess = this.chessList[this.fiveGroup[mPosition][nPosition]]; this.judgeOver(); }, judgeOver:function(){ var x0 = this.touchChess.tag % 15; var y0 = parseInt(this.touchChess.tag / 15); //判断横向 var fiveCount = 0; for(var x = 0;x < 15;x++){ if((this.chessList[y0*15+x].getComponent(cc.Sprite)).spriteFrame === this.touchChess.getComponent(cc.Sprite).spriteFrame){ fiveCount++; if(fiveCount==5){ if(this.gameState === 'black'){ this.overLabel.string = "你赢了"; this.overSprite.node.x = 0; }else{ this.overLabel.string = "你输了"; this.overSprite.node.x = 0; } this.gameState = 'over'; return; } }else{ fiveCount=0; } } //判断纵向 fiveCount = 0; for(var y = 0;y < 15;y++){ if((this.chessList[y*15+x0].getComponent(cc.Sprite)).spriteFrame === this.touchChess.getComponent(cc.Sprite).spriteFrame){ fiveCount++; if(fiveCount==5){ if(this.gameState === 'black'){ this.overLabel.string = "你赢了"; this.overSprite.node.x = 0; }else{ this.overLabel.string = "你输了"; this.overSprite.node.x = 0; } this.gameState = 'over'; return; } }else{ fiveCount=0; } } //判断右上斜向 var f = y0 - x0; fiveCount = 0; for(var x = 0;x < 15;x++){ if(f+x < 0 || f+x > 14){ continue; } if((this.chessList[(f+x)*15+x].getComponent(cc.Sprite)).spriteFrame === this.touchChess.getComponent(cc.Sprite).spriteFrame){ fiveCount++; if(fiveCount==5){ if(this.gameState === 'black'){ this.overLabel.string = "你赢了"; this.overSprite.node.x = 0; }else{ this.overLabel.string = "你输了"; this.overSprite.node.x = 0; } this.gameState = 'over'; return; } }else{ fiveCount=0; } } //判断右下斜向 f = y0 + x0; fiveCount = 0; for(var x = 0;x < 15;x++){ if(f-x < 0 || f-x > 14){ continue; } if((this.chessList[(f-x)*15+x].getComponent(cc.Sprite)).spriteFrame === this.touchChess.getComponent(cc.Sprite).spriteFrame){ fiveCount++; if(fiveCount==5){ if(this.gameState === 'black'){ this.overLabel.string = "你赢了"; this.overSprite.node.x = 0; }else{ this.overLabel.string = "你输了"; this.overSprite.node.x = 0; } this.gameState = 'over'; return; } }else{ fiveCount=0; } } //没有输赢交换下子顺序 if(this.gameState === 'black'){ this.gameState = 'white'; }else{ this.gameState = 'black'; } }});
    这里便用到了节点监听,节点数组,定时器和动态更换sprite图片。
    新建Game脚本添加到ChessBoard节点下

    3 注意课程到这里就结束了,本课程部分资源来源于网络。
    第一次写技术分享,如果大家有什么好的建议和问题请在下方留言区留言。
    4 留言 2018-11-21 20:32:10 奖励20点积分
  • 真实感图形学研究

    一、介绍真实感图形学是计算机图形的核心内容之一,是最能直接反映图形学魅力的分支。
    寻求能准确地描述客观世界中各种现象与景观的数学模型,并逼真地再现这些现象与景观,是图形学的一个重要研究课题。很多自然景物难以用几何模型描述,如烟雾、植物、水波、火焰等。本文所讨论的几种建模及绘制技术都超越了几何模型的限制,能够用简单的模型描述复杂的自然景物。
    二、自然景物模拟  在计算机的图形设备上实现真实感图形必须完成的四个基本任务。

    三维场景的描述。三维造型
    将三维几何描述转换成为二维透视图。透视变换
    确定场景中的所有可见面。消隐算法,可见面探测算法
    计算场景中可见面的颜色。根据基于光学物理的光照模型计算可见面投射到观察者眼中的光亮度大小和色彩组成

    其中三维造型技术根据造型对象分成三类:

    曲面造型:研究在计算机内如何描述一张曲面,如何对它的形状进行交互式的显示和控制。曲面造型又分成规则曲面造型(如平面、圆柱面等)和不规则曲面两种。不规则曲面造型方法主要有Bezier曲线曲面、B样条曲线曲面和孔斯曲面等
    立体造型。研究如何在计算机内定义、表示一个三维物体。这些方法主要有体素构造法、边界表示法、八叉树法等等。曲面造型和立体造型合称为几何模型造型
    自然景物模拟。研究如何在计算机内模拟自然景物,如云、水流、树等等。本文将主要集中介绍有 关自然景物模拟的有关方法

    寻求能准确地描述客观世界中各种现象与景观的数学模型,并逼真地再现这些现象与景观,是图形学的一个重要研究课题。很多自然景物难以用几何模型描述,如烟雾、植物、水波、火焰等。本文所讨论的几种建模及绘制技术都超越了几何模型的限制,能够用简单的模型描述复杂的自然景物。
    2.1 分形与IFS2.1.1 分形几何分形(fractal)指的是数学上的一类几何形体,在任意尺度上都具有复杂并且精细的结构。一般来说分形几何体都是自相似的,即图形的每一个局部都可以被看作是整体图形的一个缩小的复本。例如,雪花曲线是一种典型的分形图形,生成方法如下:取一等边三角形,在每一边中间的三分之一处分别生长出一个小的等边三角形,重复上述过程就可以形成图2.1所示的曲线。理论上来说,无限递归的结果是形成了一个有限的区域,而该区域的周长却是无限的,并且具有无限数量的顶点。这样的曲线在数学上是不可微的。
    早在19世纪就已经出现了一些据有自相似特性的分形图形,但最初只是被看作一种奇异现象。本世纪70年代,Benoit B. Mandelbrot最早对分形进行系统研究,并创立了分形几何这一新的数学分支。Mandelbrot扩展了经典欧几里得几何中的维数,提出了分数维的概念。 分形几何并不只是抽象的数学理论。例如海岸线的轮廓,如果考虑其不规则性,同样具有无限的长度。Mandelbrot认为海岸、山脉、云彩和其他很多自然现象都具有分形的特性。因此,分形几何已经成为一个发展十分迅速的科学分支,尤其是在计算机图形学中,成为描述自然景物及计算机艺术创作的一种重要手段。此外,分形在图象压缩方面也有广阔的应用前景。
    2.1.2 仿射变换与迭代函数迭代函数系统IFS (Iteration Function System)最早是由Hutchinson于1981年提出的,现已成为分形几何中的重要研究内容之一。IFS是以仿射变换为框架,根据几何对象的整体与局部具有自相似结构,经过迭代而产生的。  、
    2.1.3 基于分形的景物生成由IFS码绘出的分形图形具有无穷细微的自相似结构,能对很多客观事物作出准确的反映,这种结构是难于用经典数学模型来描述的。只要变换选取适当,利用IFS就可以迭代地生成任意精度的图形效果,这也是其他绘制方法难以做到的。
    2.2 基于文法的模型美国科学家Aristid Lindenmayer于1969年提出了一种研究植物形态与生长的描述方法,以他的名字命名为L系统(L-grammars)。1984年,A. R. Smith将L系统应用于计算机图形学中。L系统实际上是一组形式语言,由特定的语法加以描述,这些语法由一系列产生式组成,所有产生式都是直接匹配的。例如,一种典型的L系统语法包括四个字母{A,B,[,]}和两条产生式规则:
    A→AAB→A[B]AA[B]从字母A出发,可以迭代地生成A、AA、AAAA等字母序列;从字母B出发,前几步迭代结果如下:
    BA[B]AA[B]AA[A[B]AA[B]]AAAA[A[B]AA[B]]……如果我们把由这种语法规则中的产生式迭代形成的词汇看作是某种图结构的一部分,把方括号中的内容视为前一个符号的分支,则上述文法的三次迭代结果如图2.2所示。在此基础上,适当改变分支的方向,加入随机动因素及在分支的终点绘制出叶子、花、果实等细节,就可以逼地真模拟出现实世界中各种形态的植物。
    当然,上述L系统本身并没有记录任何几何信息,因此基于L系统的建模语言必须能够同时支持文法描述和几何描述;如何对L系统的生长(迭代)过程加以控制也是一个需要进行研究的问题。对此,Reffye、Prusinkiewicz等人分别提出了各自的方法。
    总之,基于文法的L系统用于植物生长过程的模拟是非常成功的,为计算机真实感图形的绘制提供了又一个有力的工具。此外,这种思想也被成功地应用到了电子线路设计和建筑设计等很多方面。
    2.3 粒子系统Reeves于1983年提出的粒子系统方法是一种很有影响的模拟不规则物体的方法,能够成功地模拟由不规则模糊物体组成的景物。与其他传统图形学方法完全不同,这种方法充分体现了不规则模糊物体的动态性和随机性,从而能够很好地模拟火、云、水、森林和原野等许多自然景象。
    粒子系统的基本思想是采用许多形状简单的微小粒子作为基本元素来表示不规则模糊物体。这些粒子都有各自的生命周期,在系统中都要经历”产生”、”运动和生长”及”消亡”三个阶段。粒子系统是一个有”生命”的系统,因此不象传统方法那样只能生成瞬时静态的景物画面,而可产生一系列运动进化的画面,这使得模拟动态的自然景物成为可能。生成系统某瞬间画面的基本步骤是:

    产生新的粒子
    赋予每一新粒子一定的属性
    删去那些已经超过生存期的粒子
    根据粒子的动态属性对粒子进行移动和变换
    显示由有生命的粒子组成的图象

    粒子系统采用随机过程来控制粒子的产生数量,确定新产生粒子的一些初始随机属性,如初始运动方向、初始大小、初始颜色、初始透明度、初始形状以及生存期等,并在粒子的运动和生长过程中随机地改变这些属性。粒子系统的随机性使模拟不规则模糊物体变得十分简便。
    三、消隐及真实感图形生成  3.1 消隐在计算机图形学中,有三种方式表示三维物体:线框图、消隐图和真实感图。其中真实感图形的生成也要在消隐基础上进行光照处理。所谓消隐就是给定一组三维对象及投影方式(视见约束),判定线、面或体的可见性的过程。根据消隐在间的不同,消隐算法可分为两类:

    物体空间的消隐算法,消隐在规范化投影空间中进行,将物体表面的k个多边形中的每一个面与其余的k-1个面进行比较,精确地求出物体上每条棱边或每个面的遮挡关系。这类算法的计算量正比于k2
    图象空间的消隐算法,消隐在屏幕坐标系中进行,对屏幕上的每一个象素进行判断,确定在该象素点上可见的面。若屏幕分辨率为m×n,物体空间中共有k个多边形,则此类算法的的计算量正比于mnk

    大多数消隐算法都涉及排序和相关性的概念。排序是为了确定消隐对象之间的遮挡关系,通常在X、Y、Z三个方向分别进行。消隐算法的效率在很大程度上取决于排序的效率。相关性是指物体对象或其变换后的图象局部保持不变的性质,在消隐算法中利用相关性是提高排序率的重要手段。
    常用的物体空间消隐算法有多边形区域排序算法和列表优先算法等。
    Z-Buffer (深度缓存)是最简单的图象空间面消隐算法,深度缓存数组的使用避免了复杂的排序过程在分辨率一定的情况下,算法计算量只与多边形个数成正比。该算法也便于硬件实现和并行处理。在此基础上,Z-Buffer扫描线算法利用了多边形边和象素点的相关性,使得算法效率进一步提高。扫描线算法也为简单光照模型提供了良好的消隐基础。
    3.2 简单光照模型及明暗处理光照模型(Illumination Model)是根据有关光学定律,计算真实感图形中各点投射到观察者眼中的光线强度和色彩的数学模型。简单的局部光照模型假定光源是点光源,物体是非透明体,不考虑折射,反射光由环境光、漫反射光和镜面反射光组成。  基于局部光照模型及明暗处理的阴影生成算法也有很多。阴影是指景物中哪些没有被光源直接照射到的按区。在计算机生成的真实感图形中,阴影可以反映画面中景物的相对位置,增加图形的立体感和场景的层次感,丰富画面的真实感效果。阴影可分为本影和半影两种。本影加上它周围的半影组成软影区。单个点光源照明只能形成本影,多个点光源和线光源才能形成半影。
    对多边形表示的物体,一种计算本影的方法是影域多边形方法,环境中物体的影域定义为视域多面体和光源在景物空间中被物体轮廓多边形遮挡的区域的交集。这种方法的实现可以利用现有的扫描线消隐算法。Athherton等人提出了曲面细节多边形方法,以多边形区域分类的隐藏面消去算法为基础,通过从光源和视点两次消隐生成阴影。
    以上两种阴影生成方法只适用于用多边形表示的景物,无法解决光滑曲面片上的阴影生成问题。为此Williams提出了Z-Buffer方法,首先利用Z-Buffer算法按光源方向对景物进行消隐,然后再用Z-Buffer算法按视线方向进行会制。这种方法可以方便地在理包括光滑曲面的任意复杂的景物,但存储量大,阴影区域附近易产生走样。
    3.3 整体光照模型与光线跟踪照射到物体上的光线,不仅有从光源直接射来的,也有经过其它物体反射或折射来的。局部光照模型只能处理直接光照,为了对环境中物体之间的各种反射、折射光进行精确模拟,需要使用整体光照模型。
    相对于局部光照模型,整体光照模型可以表示为Iglobal=KRIR+ KTIT。其中Iglobal为非直接光照对物体上一点光强的贡献;IR为其他物体从视线的反射方向R反射或折射来的光强,KR为反射系数;KT为其 他物体从视线的折射方向T折射或反射来的光强,IT为折射系数。将Iglobal与局部光照模型的计算结果相叠加,就可以得到物体上点的光强。
    光线跟踪算法是典型的整体光照模型,最早由Goldste、Nagel和Appel等人提出,Appel用光线跟踪的方法计算阴影;Whited和Kay扩展了这一算法,用于解决镜面反射和折射问题。算法的基本思想如下:
    对于屏幕上的每个象素,跟踪一条从视点出发经过该象素的光线,求出与环境中物体的交点。在交点处光线分为两支,分别沿镜面反射方向和透明体的折射方向进行跟踪,形成一个递归的跟踪过程。光线每经过一次反射或折射,由物体材质决定的反射、折射系数都会使其强度衰减,当该光线对原象素光亮度的。贡献小于给定的阈值时,跟踪过程即停止。光线跟踪的阴影处理也很简单,只需从光线与物体的交点处向 光源发出一条测试光线,就可以确定是否有其他物体遮挡了该光源(对于透明的遮挡物体需进一步处理光强的衰减),从而模拟出软影和透明体阴影的效果。
    光线跟踪很自然地解决了环境中所有物体之间的消隐、阴影、镜面反射和折射等问题,能够生成十分逼真的图形,而且算法的实现也相对简单。但是,作为一种递归算法其计算量十分巨大。尽量减小求交计 算量是提高光线跟踪效率的关键,常用的方法有:包围盒(entents)、层次结构(hierarchies)及区域分割 (spatial partitioning)等技术。
    光线跟踪是一个典型的采样过程,各个屏幕象素的亮度都是分别计算的,因而会产生走样,而算法本身的计算量使得传统的加大采样频率的反走样技术难以实用。
    象素细分是一种适用于光线跟踪的反走样技术,具体方法是: 首先对每一象素的角点用光线跟踪计算亮度;然后比较各角点的亮度,若差异较大,则将象素细分为4个子区域,并对新增的5个角点用光线跟踪计算亮度;重复比较与细分,直到子区域各角点亮度差异小于给定的阀值为止;最后加权平均求出象素点的显示亮度。
    与象素细分不同,Cook、Porter和Carpenter 提出的分布式光线跟踪是一种随机采样的方法,在交点处镜面反射方向和折射方向所夹的立体角内,按照一定的分布函数同时跟踪若干根光线,然后进行加权平均。Cook等人还提出了利用分布式随机采样技术模拟半影、景深和运动模糊等效果的方法。
    光线跟踪的另一个问题是,光线都是从视点发出的,阴影测试光线则需另外处理,因而无法处理间接的反射或折射光源,例如镜子或透镜对光源所产生的作用就难以模拟。为解决这一问题,可以从光源和视点出发对光线进行双向跟踪。但是,大量从光源出发的光线根本不可能到达屏幕,这使得双向光线跟踪的计算量显著增大,难以实用。Heckbert和Hanrahanr提出的解决方法是只将从光源出发的光线跟踪作为常归光线跟踪的补充;Arvo方法则是对从光源发出进入环境的光线进行预处理;邵敏之和彭群生等人也提出 了基于空间线性八叉树结构的对光源所发出光线进行优化的双向光线跟踪算法。
    3.4 漫反射和辐射度方法常规光照模型假设物体间的漫反射是一个恒定的环境光,即使双向光线跟踪也只能处理物体间的反射与折射,而不能处理物体间的漫反射。最初由Goral等人于1984年及Nishita等人于1985年提出的辐射度方法是一种基于热能工程的方法,用光辐射的产生和反射代替环境光,从而能够精确处理对象之间的光反射问题。
    辐射度方法将景物和光源视为一个封闭的系统,在系统中光能量是守衡的;并假定构成景物的曲面都是理想的漫反射面。所谓辐射度,是指单位时间内从曲面上单位面积反射出去的光能量,记为B。在理想情况下,可以近似认为逼近曲面的面片上的光强是均匀的,即漫反射各向均匀。根据能量守衡定律
    辐射度方法的主要计算量在于计算形状因子。Cohen和Greenberg提出的半立方体方法是一种近似计算 封闭面形状因子的高效方法。首先以面片i的中心为原点,法向量为Z轴建立一个半立方体,将其五个表面划分成均匀的网格,每个网格单元的微形状因子可以预先求得;然后将场景中所有其他面片都投影到半立方体上,对于多个面片投影到同一个网格单元的情况需在投影方向上进行深度比较,网格单元只保留最近的面片,这一过程相当于Z-Buffer算法;最后将半立方体中所有与面片j相关的网格单元的微形状因子累加,即可得到面片i相对于面片j的形状因子Fij。 辐射度方法的优点在于算法与视点无关、计算和绘制可以分别独立进行、能够模拟颜色渗透效果等,但无法处理镜面反射与折射。
    在辐射度方法中,面片向特定方向辐射出的光能量仅总辐射度有关,而与所接受能量的方向无关。Immel、Cohen和Greenberg推广了这一方法,每个面片不只计算唯一的辐射度,而是将面片半球空间分割成有限个空间立体角的区域,在每个区域内分别计算输入输出的光能量,通过双向辐射函数计算向某一方向辐射能量的概率,每个顶点的光强可以通过对与视点方向最为接近的若干方向上的辐射度进行插值得到,并最终完成图形生成。这种改进方法可以处理包含镜面和透明物体的复杂场景,但要付出巨大的时间开销和空间开销。
    另一种方案是将辐射度与光线跟踪相结合,仅仅将两种方法的计算结果相加是不够的,必须同时处理漫反射面和镜面之间的光照关系。Wallace、Cohen和Greenberg提出了一种两步方法:

    第一步执行与视点无关的辐射度方法,辐射度的计算必须考虑镜面,这可以通过镜象法(mirror-world approach)予以模拟
    第二步执行基于视点的光线跟踪算法,处理整体镜面反射和折射,并生成图形。算法效率的关键在于第一步,其中镜象法只需处理理想镜面的反射作用,并据此对形状因子加以修正,形状因子的计算量将随镜面数量的增加而显著增加

    Sillon和Puech进一步扩展了上述两步法,在第一步时不采用镜象法,而是用递归的光线跟踪来计算形状因子,可以处理具有任意数量镜面及透明体的场景。
    3.5 纹理映射纹理映射(Texture Mapping)是通过将数字化的纹理图象覆盖或投射到物体表面,从而为物体表面增加表面细节的过程。纹理图象可以通过采样得到,也可以通过数学函数生成。物体的很多表面细节多边形逼近或其他几何建模的方法是难以表现的,因此纹理映射技术能够使得计算机生成的物体看起来更加逼真自然。
    纹理映射技术最早由Catmull提出,经Blinn和Newell改进后得到广泛应用,成为计算机图形学中的一种重要方法。将纹理映射到物体表面,可以看作是将一个屏幕象素投影到纹理空间的对应区域并计算该区域的平均颜色,以求得真正象素颜色的最佳近似值。具体地说,纹理图象存在于独立的纹理空间中,映射分为两步进行,先将屏幕象素通过四个角点坐标映射到三维物体表面,再进一步映射到纹理空间,形成一个四边形区域,即对屏幕象素映射到三维物体表面上所形成的复杂曲面片的近似。屏幕象素的纹理映射结果可以通过对纹理空间中四边形区域进行累加得到。也可以采用相反的映射方式,即从纹理空间到三维物 体再到屏幕象素进行映射,但这种映射方式需要占用更大的存储空间,更易产生走样,并且无法应用于扫描线算法。
    物体表面的纹理可分为两类:颜色纹理和几何纹理。颜色纹理主要是指同一表面各处呈现出不现的花纹和颜色;几何纹理主要指物体表面在微观上呈现出的起伏不平。上述纹理映射方法只能处理颜色纹理,所生成的物体表面仍然是光滑的。Blinn在纹理映射基础上提出的Bump Mapping方法是一种模拟物体表面粗糙纹理的技术,可以不用对物体的粗糙表面在几何上进行建模就可以改善物体表面的微观结构,如大理石纹理表面雕刻的文字、混凝土墙面等效果。此外,更高级的真实感图形效果如人脸上流淌的汗水也可以通过随时间变化的Bump mapping来模拟。Bump Map是一个二维数组,数组中每个元素是物体表面上一点比 其实际位置略高或略低的偏移向量。这些微小偏移被映射到物体表面一点后修正该点处的法向量,通过修正后的法向量进行光照计算。
    纹理图象和屏幕象素都是离散的采样系统,很容易产生走样,即丢失纹理细节,使表面边界产生锯齿。纹理映射中常用的反走样方法是卷积滤波法。屏幕象素是一个矩形区域,映射到纹理空间上为一任意四 边形,卷积滤波法就是取四边形所覆盖区域的纹理函数的卷积作为屏幕象素的光亮度,可以采用盒形、三角形、高斯分布及样条函数等作为滤波函数。在实际应用中为简化计算,常用正方形、矩形或椭圆等形状近似表示屏幕象素所覆盖的任意四边形区域。卷积滤波法是非线性的,计算量较大,并且不适用于Bump Mapping,因为Bump Mapping的纹理函数与象素的光亮度之间不是线性关系,此时可以使用前置滤波法。前置滤波是在纹理空间中按照不同的分辨率将一定区域内的纹理平均值预先算好,执行映射时只需按照屏幕象素所覆盖的区域大小选取一定的分辨率查表,并作适当线性插值即可。
    以上二维映射在很多情况下都能得到很好的效果,但有时会产生失真,如在三维曲面上仍会呈现出二维效果,以及产生纹理接缝问题等。Peachey和Perlin提出了一种基于实体纹理的方法,用物体上点在三维空间的位置的函数作为纹理,从而更精确地表现木材或大理石等的雕刻效果。
    其他一些材质的表面也可以用适当的方法模拟,如Gardner的透明映射方法可以用简单的形状模拟云彩。此外,很多基于物理模型、随机过程和分形几何等的方法也被用来生成自然纹理。
    1 留言 2019-06-27 11:19:59 奖励16点积分
  • 机器学习 24 、MF ANN

    前文链接:https://write-bug.com/article/2696.html
    MF(Matrix Factorization)基于矩阵分解的推荐算法-隐语义模型:ALS、LFM、SVD、SVD++
    在15年左右流行
    ALS:-交替最小二乘
    我们之前学习的协同过滤CF:单独考虑user、item
    这里同时考虑User-item两方面:
    原来我们的UI稀疏打分矩阵\<m,n>:

    一般公司用户量可以随意达到上亿,物品量如音乐可以达到几十万,用户量之所以多,是因为可能一个用户有多个账户等等,共同组成一个很大很稀疏的矩阵那么面对这样一个矩阵,我们可以通过矩阵分解来解决:
    将User和Item分别映射到新的空间,分解成两个矩阵,U 和I两个维度都不变



    K值远小于M和N,从而达到降维目的
    无需关注新空间的各个维度,只需假定存在 ,即用向量表示user和item
    新的维度称为Latent Factor(隐因子)

    K的维度相比原来来说很小很小,并且可以人为设定,只需要两个矩阵相乘就能得到UI矩阵,即R’ ≈R
    两个矩阵相似如何界定?误差
    误差表示:RMSE :均方根误差
    目标:真实矩阵,和结果矩阵之间的尽可能逼近
    最小化平方误差作为损失函数:

    考虑矩阵稳定性,引入L2正则项,得到损失函数:

    这里的rui就是上面所说的R,即原矩阵user对item打的分数
    xuyi即分解矩阵后再相乘的预估分数,两个相减就是误差
    未知数:
    Xu:user vectoryi:item vector如何求解最优值:求导=0
    公式1:

    导数为0,可得到:

    同理对yi求导(公式2):

    为什么叫交替二乘法?
    这里的做法是让x和y交替下降:

    随机生成X、Y向量(初始化)
    固定Y,更新X(公式1)
    固定X,更新Y(公式2)
    第2、3步循环,直到满足收敛条件(RMSE)

    ——均方根误差
    那么我们得到这两个小矩阵,可以做什么?
    item-item :IK*KI=IIUser-User:UK*KU=UUuser与item的向量
    LFM思路和ALS算法类似,区别在于,ALS利用坐标下降法,LFM利用梯度下降法
    假设:
    评分矩阵𝑅𝑚,𝑛,m个用户对n个物品评分

    𝑟𝑢,𝑖:用户u对物品i的评分
    𝑅𝑚,𝑛 = 𝑃𝑚,𝐹 ∙ 𝑄𝐹,𝑛:R是两个矩阵的乘积
    P:每一行代表一个用户对各隐因子的喜欢程度 ,即前面的user矩阵
    Q:每一列代表一个物品在各个隐因子上的概率分布,即前面的item矩阵


    在之前的随机梯度下降中,我们更新w(t+1)=w(t) - a*g(梯度)
    那么根据这样的思路,这里把矩阵P与Q的分数当作权重w来更新:

    矩阵展开后相当于对每个元素分数进行偏导:

    随意抽取中间这个元素作为代表,求取偏导:

    由于

    所以后面两个等式相等,之后就有一件很有意思的事情发生了:P的分数是由上一时刻的Q更新的,Q的分数是由上一时刻的P更新的。也就是类似上面的坐标下降法:交替进行。
    代入原式:

    LFM实践:
    class LFM(object): def __init__(self, rating_data, F, alpha=0.1, lmbd=0.1, max_iter=500): '''rating_data是list<(user,list<(position,rate)>)>类型 ''' self.F = F#维度K self.P = dict() self.Q = dict() self.alpha = alpha#学习率 self.lmbd = lmbd self.max_iter = max_iter#迭代轮数 self.rating_data = rating_data#矩阵 '''随机初始化矩阵P和Q''' for user, rates in self.rating_data: self.P[user] = [random.random() / math.sqrt(self.F) for x in xrange(self.F)] for item, _ in rates: if item not in self.Q: self.Q[item] = [random.random() / math.sqrt(self.F) for x in xrange(self.F)] def train(self): '''随机梯度下降法训练参数P和Q ''' for step in xrange(self.max_iter): for user, rates in self.rating_data: for item, rui in rates: hat_rui = self.predict(user, item) err_ui = rui - hat_rui for f in xrange(self.F): self.P[user][f] += self.alpha * (err_ui * self.Q[item][f] - self.lmbd * self.P[user][f]) self.Q[item][f] += self.alpha * (err_ui * self.P[user][f] - self.lmbd * self.Q[item][f]) self.alpha *= 0.9 # 每次迭代步长要逐步缩小 def predict(self, user, item): '''预测用户user对物品item的评分 ''' return sum(self.P[user][f] * self.Q[item][f] for f in xrange(self.F))if __name__ == '__main__': '''用户有A B C,物品有a b c d,列表模拟矩阵:''' rating_data = list() rate_A = [('a', 1.0), ('b', 1.0)] rating_data.append(('A', rate_A)) rate_B = [('b', 1.0), ('c', 1.0)] rating_data.append(('B', rate_B)) rate_C = [('c', 1.0), ('d', 1.0)] rating_data.append(('C', rate_C)) lfm = LFM(rating_data, 2) lfm.train() for item in ['a', 'b', 'c', 'd']: print(item, lfm.predict('A', item)) # 预测计算用户A对各个物品的喜好程度SVDLFM有什么缺点?没有考虑客观的偏置,所以带偏置的LFM称为SVD
    什么是偏置,比如说有的人很极端给一些物品很高或者很低的分数,而有的人给每个物品都很平均的分数,还有包括地区等等都会影响对物品的看法,所以就有一个偏置存在。
    偏置:事件固有的,不受外界影响的属性。

    𝜇:训练集中所有评分的平均值
    𝑏𝑢:用户偏置,代表一个用户评分的平均值
    𝑏𝑖:物品偏置,代表一个物品被评分的平均值


    更新:

    SVD实践:
    class BiasLFM(object): def __init__(self, rating_data, F, alpha=0.1, lmbd=0.1, max_iter=500): '''rating_data是list<(user,list<(position,rate)>)>类型 ''' self.F = F self.P = dict() self.Q = dict() self.bu = dict() self.bi = dict() self.alpha = alpha self.lmbd = lmbd self.max_iter = max_iter self.rating_data = rating_data self.mu = 0.0 '''随机初始化矩阵P和Q''' cnt = 0 for user, rates in self.rating_data: self.P[user] = [random.random() / math.sqrt(self.F) for x in xrange(self.F)] self.bu[user] = 0#初始化bu cnt += len(rates) for item, rate in rates: self.mu += rate if item not in self.Q: self.Q[item] = [random.random() / math.sqrt(self.F) for x in xrange(self.F)] self.bi[item] = 0#初始化bi self.mu /= cnt#计算μ def train(self): '''随机梯度下降法训练参数P和Q ''' for step in xrange(self.max_iter): for user, rates in self.rating_data: for item, rui in rates: hat_rui = self.predict(user, item) err_ui = rui - hat_rui #更新两个b self.bu[user] += self.alpha * (err_ui - self.lmbd * self.bu[user]) self.bi[item] += self.alpha * (err_ui - self.lmbd * self.bi[item]) for f in xrange(self.F): self.P[user][f] += self.alpha * (err_ui * self.Q[item][f] - self.lmbd * self.P[user][f]) self.Q[item][f] += self.alpha * (err_ui * self.P[user][f] - self.lmbd * self.Q[item][f]) self.alpha *= 0.9 # 每次迭代步长要逐步缩小 def predict(self, user, item): '''预测用户user对物品item的评分,加偏置 ''' return sum(self.P[user][f] * self.Q[item][f] for f in xrange(self.F)) + self.bu[user] + self.bi[item] + self.muif __name__ == '__main__': '''用户有A B C,物品有a b c d''' rating_data = list() rate_A = [('a', 1.0), ('b', 1.0)] rating_data.append(('A', rate_A)) rate_B = [('b', 1.0), ('c', 1.0)] rating_data.append(('B', rate_B)) rate_C = [('c', 1.0), ('d', 1.0)] rating_data.append(('C', rate_C)) lfm = BiasLFM(rating_data, 2) lfm.train() for item in ['a', 'b', 'c', 'd']: print(item, lfm.predict('A', item)) # 计算用户A对各个物品的喜好程度SVD++任何用户只要对物品i有过评分,无论评分多少,已经在一定程度上反映了用户对各个隐因子的喜好 程度𝑦𝑖 = (𝑦𝑖1,𝑦𝑖2,…,𝑦𝑖𝐹),y是物品携带的属性,什么意思?比如说A买了3个item,B买了3个item,每个item背后有一系列feature vector,那么我们用A买的3个item背后的fea向量相加(实际计算是学习更新出来的,不一定是相加)代表一个虚拟的物品A,间接表达了这个用户的偏好程度,同理得到向量B,那么对于这个每个人背后的虚拟物品向量,就是y

    所以这个打分是在P上又增加了一个类似于偏置的东西,并做了归一化

    𝑁(𝑢):用户u评价过的物品集合
    𝑏𝑢:用户偏置,代表一个用户评分的平均值
    𝑏𝑖:物品偏置,代表一个物品被评分的平均值


    SVD++实践:
    class SVDPP(object): def __init__(self, rating_data, F, alpha=0.1, lmbd=0.1, max_iter=500): '''rating_data是list<(user,list<(position,rate)>)>类型 ''' self.F = F self.P = dict() self.Q = dict() self.Y = dict() self.bu = dict() self.bi = dict() self.alpha = alpha self.lmbd = lmbd self.max_iter = max_iter self.rating_data = rating_data self.mu = 0.0 '''随机初始化矩阵P、Q、Y''' cnt = 0 for user, rates in self.rating_data: self.P[user] = [random.random() / math.sqrt(self.F) for x in xrange(self.F)] self.bu[user] = 0 cnt += len(rates) for item, rate in rates: self.mu += rate if item not in self.Q: self.Q[item] = [random.random() / math.sqrt(self.F) for x in xrange(self.F)] if item not in self.Y:#比之前svd多加了一个y,初始化y self.Y[item] = [random.random() / math.sqrt(self.F) for x in xrange(self.F)] self.bi[item] = 0 self.mu /= cnt def train(self): '''随机梯度下降法训练参数P和Q ''' for step in xrange(self.max_iter): for user, rates in self.rating_data: z = [0.0 for f in xrange(self.F)] for item, _ in rates: for f in xrange(self.F): z[f] += self.Y[item][f]#用户观看过物品的评分集合加和,即虚拟物品向量,即∑Yjf ru = 1.0 / math.sqrt(1.0 * len(rates)) s = [0.0 for f in xrange(self.F)] for item, rui in rates: hat_rui = self.predict(user, item, rates) err_ui = rui - hat_rui self.bu[user] += self.alpha * (err_ui - self.lmbd * self.bu[user]) self.bi[item] += self.alpha * (err_ui - self.lmbd * self.bi[item]) for f in xrange(self.F): s[f] += self.Q[item][f] * err_ui#每个物品的信息和误差相乘的累加 self.P[user][f] += self.alpha * (err_ui * self.Q[item][f] - self.lmbd * self.P[user][f]) self.Q[item][f] += self.alpha * ( err_ui * (self.P[user][f] + z[f] * ru) - self.lmbd * self.Q[item][f]) for item, _ in rates: for f in xrange(self.F): #Y的更新 self.Y[item][f] += self.alpha * (s[f] * ru - self.lmbd * self.Y[item][f]) self.alpha *= 0.9 # 每次迭代步长要逐步缩小 def predict(self, user, item, ratedItems): '''预测用户user对物品item的评分 ''' z = [0.0 for f in xrange(self.F)] for ri, _ in ratedItems: for f in xrange(self.F): z[f] += self.Y[ri][f] return sum( (self.P[user][f] + z[f] / math.sqrt(1.0 * len(ratedItems))) * self.Q[item][f] for f in xrange(self.F)) + \ self.bu[user] + self.bi[item] + self.muif __name__ == '__main__': '''用户有A B C,物品有a b c d''' rating_data = list() rate_A = [('a', 1.0), ('b', 1.0)] rating_data.append(('A', rate_A)) rate_B = [('b', 1.0), ('c', 1.0)] rating_data.append(('B', rate_B)) rate_C = [('c', 1.0), ('d', 1.0)] rating_data.append(('C', rate_C)) lfm = SVDPP(rating_data, 2) lfm.train() for item in ['a', 'b', 'c', 'd']: print(item, lfm.predict('A', item, rate_A)) # 计算用户A对各个物品的喜好程度ANNANN 多维空间检索算法,不完全是算法,是更偏工程的一种方法,时下正在流行的简单方式,从图像检索演化而来,复杂一点的方式一般使用DNN可以达到目的每个用户、物品背后都有自己的向量映射在多为空间的点上,我们的目标就是把这些向量全部映射在一个空间内,求user最近的item点
    稀疏场景适用物品召回:cb倒排(token)、cf倒排(user)
    —召回能力有限
    鲜花和巧克力在情人节的情况下可以关联起来,但是通过cb不能召回,通过cf需要很多很多用户共点击或者共现才会关联
    那么这里如何计算和user距离近的点?之前我们使用cos或jaccard,但是我不能把所有的物品都遍历一遍,计算量过大,所以就引出了ANN的近邻检索annoy
    Annoy目标:建立一个数据结构,在较短的时间内找到任何查询点的最近点,在精度允许的条件下通过牺牲准 确率来换取比暴力搜索要快的多的搜索速度
    先圈出一部分集合,遍历集合取top
    如果推荐的结果不理想,不喜欢,不是这个方法的问题,而是embedding方式的问题
    那么这个集合如何圈呢?
    方案:随机选择两个点,然后根据这两个点之间的连线确定一个可以垂直等分线段的超平面,灰色是两点的连 线,黑色是超平面

    从而由里面的点集合就构成了叶子节点,最终形成整棵树
    建立二叉树结构用于表示空间分布,每一个节点表示一个子空间
    假设,如果两个点在空间彼此靠近,任何超平面不会将他们分开,如果我们搜索空间中的任意一点,和其距离 近的点为推荐候选,通过对二叉树的遍历,可以完成
    重复上页步骤,继续分割 ,过程很像Kmeans要求:每个子节点包含数据不超过K个(10):


    如果落在这个空间内的节点候选太少怎么办?或者本来两个点距离很近,但是被两个空间分割导致不能计算,那么我们就拓宽区域、多建几棵树,每次建立长得样子都不太一样。
    annoy只是多维空间检索中的一种方法,其他还有:

    KD-Tree (KNN-开始不随机,直接找到全局最优,速度慢,建树慢)
    局部敏感哈希LSH
    HNSW
    OPQ等等

    ANN实践:
    annoy安装:pip
    import sysfrom annoy import AnnoyIndexa = AnnoyIndex(3)#建立3颗树a.add_item(0, [1, 0, 0])#添加用户和item点a.add_item(1, [0, 1, 0])a.add_item(2, [0, 0, 1])a.build(-1)print(a.get_nns_by_item(0, 100))#与0这个用户最近的top100集合print(a.get_nns_by_vector([1.0, 0.5, 0.5], 100))#与这个item vector 最近的top100集合下面这个代码是什么意思呢?随着我们候选集合圈的缩小,我们的计算量也在缩小,那么我们目标是让这个候选集合的top和全局的top尽量一致,也就是说要在计算量和召回率之间做一个权衡,那么全局的top我们就只能通过暴力遍历来知道答案了。
    from __future__ import print_functionimport random, timefrom annoy import AnnoyIndextry: xrangeexcept NameError: # Python 3 compat xrange = rangen, f = 100000, 40#初始化10w个点,40颗树t = AnnoyIndex(f)for i in xrange(n): v = [] for z in xrange(f): v.append(random.gauss(0, 1))#高斯分布初始化点 t.add_item(i, v)#添加在树里t.build(2 * f)t.save('test.tree')#保存树limits = [10, 100, 1000, 10000]#圈的大小k = 10#10个候选prec_sum = {}prec_n = 100time_sum = {}for i in xrange(prec_n): j = random.randrange(0, n)#随机选一个点作为用户 #print(j) closest = set(t.get_nns_by_item(j, k, n))#求取与这个用户j最近的全局top10 #print(closest) for limit in limits: t0 = time.time() toplist = t.get_nns_by_item(j, k, limit)#圈内的top10 T = time.time() - t0 found = len(closest.intersection(toplist))#用全局与圈内的top取交集 hitrate = 1.0 * found / k#准确率 #print(hitrate) prec_sum[limit] = prec_sum.get(limit, 0.0) + hitrate time_sum[limit] = time_sum.get(limit, 0.0) + T print(prec_sum[limit])for limit in limits: print('limit: %-9d precision: %6.2f%% avg time: %.6fs' % (limit, 100.0 * prec_sum[limit] / (i + 1), time_sum[limit] / (i + 1)))
    0 留言 2019-06-24 11:08:26 奖励15点积分
  • 深度学习 19、DNN

    前文链接:https://write-bug.com/article/2540.html
    深度学习在前几节介绍的模型都属于单点学习模型,比如朴素贝叶斯的模型为概率,lr与softmax的模型为w,b权重,聚类kmeans的模型为中心向量点,还有后面的决策树的树模型等等都属于单点学习模型,都有各自的用途,而这里的深度学习来说,属于机器学习的一部分,可用不同的网络结构来解决各个领域的问题,模型为深度学习网络。
    通过前几节的学习,不知道大家有没有对单点学习的模型有感触,所谓模型是什么?
    我们从已有的例子(训练集)中发现输入x与输出y的关系,这个过程是学习(即通过有限的例子发现输入与输出之间的关系),而我们使用的function就是我们的模型,通过模型预测我们从未见过的未知信息得到输出y,通过激活函数(常见:relu,sigmoid,tanh,swish等)对输出y做非线性变换,压缩值域,而输出y与真实label的差距就是误差error,通过损失函数再求取参数偏导,梯度下降降低误差,从而优化模型,我们前面见过的损失函数是mse和交叉熵

    而DNN的模型就是网络结构,在说如何选取更好的网络结构之前,我们先介绍下神经网络:

    人类大脑的神经网络如上图具象出来就是无数个神经元,而神经元分为几个结构:

    树突:接收信号源
    神经元:核心计算单元
    轴突:传输数据到下一个神经元

    通过一个个神经元的传输,生成结果,比如说条件反射:缩手
    而在我们模型中,这个网络怎么表示呢?

    感知机:最最简单的神经网络,只有一个神经元


    树突:样本特征a1
    轴突:权重w1

    树突与轴突,也就是样本特征和权重相乘,有些类似lr的wx,共同决策了神经元的输入

    神经元:对信号进一步处理,比如lr中wx的∑,并加入偏置b,默认为1
    传递函数(激活函数):对f(s)做压缩非线性变换

    假设一个二分类问题:

    输入:
    树突P1:颜色
    树突P2:形状
    香蕉(黄色,弯形):(-1,-1)
    苹果(红色,圆形):(1,1)

    相当于坐标轴的两个点,我们需要一个分类面把两个点分割开。

    P1颜色的取值为1,-1
    P2形状的取值为1,-1
    初始化w1=w2=1,b=0

    则有:
    s=p1w1+p2w2=2>0s=p2 2w2 2+p2 2w2 2=-2 < 0这个初始化的参数可以区分,但是初始化有随机性,换一组参数就不能区分了
    所以感知机的训练方法,目的就是训练权重和偏置
    w=w+epb=b+ee(误差)=t-a=期望-实际
    如,假设w1=1,w2=-1,b=0

    苹果s=0
    期望为1,e=1-0=1

    修正:
    w1=1+1*1=2w2=-1+1*1=0b=0+1=1s=3>0为苹果但是,y=wx+b只能解决线性可分的问题,对于异或问题,也就是四点对立面,需要一个曲线做分割,而这就意味着需要多层神经元和信号源共同决策分割面,也就形成了复杂的神经网络,而目标:同样是那么多边的权重。

    通过不同层间的结果作为下一层的输入,再对输入中间结果进一步处理,以此类推形成足够的深度,对于大部分问题来说都可以提到一个提升效果的作用。
    感知机只有从输出层具有激活函数,即功能神经元,这里的隐层和输出层都是具有激活函数的功能神经元。
    层与层全连接,同层神经元不连接,不跨层连接的网络又称为多层前馈神经网络。
    前馈:网络拓扑结构上不存在环和回路
    我们通过pytorch实现演示:
    二分类问题:
    假数据准备:
    # make fake data#正态分布随机产生n_data = torch.ones(100, 2)x0 = torch.normal(2*n_data, 1) # class0 x data (tensor), shape=(100, 2)y0 = torch.zeros(100) # class0 y data (tensor), shape=(100, 1)x1 = torch.normal(-2*n_data, 1) # class1 x data (tensor), shape=(100, 2)y1 = torch.ones(100) # class1 y data (tensor), shape=(100, 1)#拼接数据x = torch.cat((x0, x1), 0).type(torch.FloatTensor) # shape (200, 2) FloatTensor = 32-bit floatingy = torch.cat((y0, y1), ).type(torch.LongTensor) # shape (200,) LongTensor = 64-bit integer# tensor类型 :张量:高维特征,不可更新#variable类型:可变参数,更新w,b从而更新xy# torch can only train on Variable, so convert them to Variablex, y = Variable(x), Variable(y)设计构建神经网络:class net 继承torch.nn.model
    200\*2, 2\*10, 10\*2参数:n_fea=2,n_hidden=10,n_output=2
    输入层2
    初始化hidden层10
    self.hidden=torch.nn.linear(n_fea,n_hidden)输出层2
    self.out=torch.nn.linear(n_hidden,n_output)class Net(torch.nn.Module): def __init__(self, n_feature, n_hidden, n_output): super(Net, self).__init__() self.hidden = torch.nn.Linear(n_feature, n_hidden) # hidden layer self.out = torch.nn.Linear(n_hidden, n_output) # output layer#激活函数,在隐藏层后加relu非线性变换 def forward(self, x): x = F.relu(self.hidden(x)) # activation function for hidden layer x = self.out(x) return x #输出的预测值xnet = Net(n_feature=2, n_hidden=10, n_output=2) # define the networkprint(net)参数梯度下降sgd:
    optimizer = torch.optim.sgd(net.parameters(),lr=0.02)损失函数:交叉熵:负对数似然函数,并计算softmax
    loss_func = torch.nn.CrossEntropyLoss()参数初始化:
    optimizer.zero_grad()反向传播:
    loss.backward()计算梯度:
    optimizer.step()for t in range(100): out = net(x) # input x and predict based on x loss = loss_func(out, y) # must be (1. nn output, 2. target), the target label is NOT one-hotted optimizer.zero_grad() # clear gradients for next train loss.backward() # backpropagation, compute gradients optimizer.step() # apply gradients if t % 2 == 0: # plot and show learning process plt.cla() prediction = torch.max(F.softmax(out), 1)[1] pred_y = prediction.data.numpy().squeeze() target_y = y.data.numpy() plt.scatter(x.data.numpy()[:, 0], x.data.numpy()[:, 1], c=pred_y, s=100, lw=0, cmap='RdYlGn') accuracy = sum(pred_y == target_y)/200. plt.text(1.5, -4, 'Accuracy=%.2f' % accuracy, fontdict={'size': 20, 'color': 'red'}) plt.pause(0.1)plt.ioff()plt.show()交叉熵问题:
    二次代价:

    a 是 神经元的输出,其中 a = σ(z), z = wx + b
    C求导:w,b求偏导:
    受到激活函数影响

    交叉熵函数:


    不用对激活函数导数影响,不管是对w求偏导还是对b求偏导,都和e有关,而和激活函数的导数无关,比如说sigmoid函数,只有中间下降最快,两边的下降逐渐缓慢,最后斜率逼近于直线
    反向传播算法:errorBP
    可应用于大部分类型神经网络
    一般说BP算法时,多指多层前馈神经网络
    上面的数据为了演示,造的假数据形成200*2维的矩阵,一列label
    那么如何对接我们的真实数据呢?

    在我们前几节的数据中大都是这种形式,label feature_id:score
    定义一个数组
    output =[None]*3 维护3列tensor数据:
    max_fid = 123num_epochs = 10data_path = '/root/7_codes/pytorch_test/data/a8a'all_sample_size = 0sample_buffer = []with open(data_path, 'r') as fd: for line in fd: all_sample_size += 1 sample_buffer.append(line.strip())output = [None] *3#idids_buffer = torch.LongTensor(all_sample_size, max_fid)ids_buffer.fill_(1)output[0] = ids_buffer#weightweights_buffer = torch.zeros(all_sample_size, max_fid)output[1] = weights_buffer#labellabel_buffer = torch.LongTensor(all_sample_size)output[2] = label_bufferrandom.shuffle(sample_buffer)sample_id = 0for line in sample_buffer: it = line.strip().split(' ') label = int(it[0]) f_id = [] f_w = [] for f_s in it[1:]: f, s = f_s.strip().split(":") f_id.append(int(f)) f_w.append(float(s)) #解析数据后填入数组 f_id_len = len(f_id)#适当拓展数组大小 output[0][sample_id, 0:f_id_len] = torch.LongTensor(f_id) output[1][sample_id, 0:f_id_len] = torch.FloatTensor(f_w) output[2][sample_id] = label sample_id += 1fid, fweight, label = tuple(output)fid, fweight, label = map(Variable, [fid, fweight, label])接下来是设计网络结构:

    def emb_sum(embeds, weights): emb_sum = (embeds * weights.unsqueeze(2).expand_as(embeds)).sum(1) return emb_sum#输入结构为22696条数据*124维特征*32维特征向量:可以将其想象成立方体的三维条边#想要把id的特征向量和对应的score相乘并相加,需要把score也就是权重weight拓展成和id同样的维度,即22696 *124 *32 ,而sum(1)就是把124维相加成1维,最后形成22696×32维的平面矩阵,即每个样本后有个32维的向量存在,相当于把feature_id向量化class Net(torch.nn.Module): def __init__(self, n_embed, n_feature, n_hidden, n_output): super(Net, self).__init__() self.embedding = nn.Embedding(max_fid + 1, n_feature) #124特征*32隐层向量 self.hidden = torch.nn.Linear(n_feature, n_hidden) # hidden layer self.out = torch.nn.Linear(n_hidden, n_output) # output layer def forward(self, fea_ids, fea_weights): embeds = self.embedding(fea_ids) embeds = emb_sum(embeds, fea_weights)#对特征进一步处理,融合id与weight_score进行相容 embeds = nn.functional.tanh(embeds)#非线性变换 hidden = self.hidden(embeds) output = self.out(hidden) return outputnet = Net(n_embed = 32, n_feature= 32, n_hidden=10, n_output=2) # define the networkprint(net)以上,我们对接了真实的数据类型,那这个demo有没有什么问题呢?
    1.是否支持大规模数据,进行快速训练?mini-batch
    我们之前学BGD、SGD、MGD梯度下降的训练方法,在上面就运用了sgd的方法,不管是BGD还是SGD都是对所有样本一次性遍历一次,如果想提升,大致相当于MGD的方法:
    把所有样本分批处理,每批次有多少个样本(batch),循环所有样本循环多少轮(epoch)。
    每训练一个batch产生一条log,参数可保存,之前是每epoch输出一次log,这种方法训练快,性能虽然赶不上全局跑出来的准确率,但仍然一直逼近,小步快跑,每次加载到内存的数据较小,性能方面就高。
    num_epochs=10batch_size =1000data_path = './data/a8a'max_fid = 123def read_and_shuffle(filepath,shuffle=True): lines=[] with open(filepath,’r’) as fd: for linr in fd: lines.append(line.strip()) if shuffle: random.shuffle(lines) return linesclass DataLoader(object): def __init__(self,data_path,batch_size,shuffle = True): self.batch_size = batch_size self.shuffle = shuffle if os.path.isdir(data_path): self.files = [ join_path(data_path,i) for I in os.listdir(data_path)] else: self.files = [data_path] def parse(self,line_ite): for line in line_ite: it = line.strip().split(‘ ’) label = int(it[0]) f_id = [] f_w = [] for f_s in it[1:]: f,s = f_s.strip().split(‘:’) f_id.append(int(f)) f_w.append(float(s)) f_id_len = len(f_id) output = [] output.append(f_id) output.append(f_w) output.append(f_id_len) output.append(label) yield output def to_tensor(self,batch_data,sample_size): output =[None]*3 ids_buffer = torch.LongTensor(sample_size,max_fid) ids_buffer.fill_(1) output[0]=ids_buffer weights_buffer = torch.zeros(sample_size,max_fid) output[1]=weights_buffer label_buffer = torch.LongTensor(sample_size) output[2] = label_buffer for sample_id,sample in enumerate(batch_data): f_id,f_weight,f_id_len,label = sample output[0][sample_id,0:f_id_len] = torch.LongTensor(f_id) output[1][sample_id,0:f_id_len] = torch.FloatTensor(f_w) output[2][sample_id] = label return tuple(output) def batch(self): count = 0 batch_buffer =[] for f in self.files: for parse_res in self.parse(read_and_shuffle(f,self.shuffle)): count+=1 batch_buffer.append(parse_res) if count ==self.batch_size: t_size = len(batch_buffer) batch_tensor = self.to_tensor(batch_buffer,t_size) yield (batch_tensor,t_size) count = 0 batch_buffer = [] if batch_buffer: t_size = len(batch_buffer) batch_tensor = self.to_tensor(batch_buffer,t_size) yield (batch_tensor,t_size)for epoch in range(1,num_epochs+1): for (batch_id,data_tuple) in enumrate(dataloader.batch(),1): data = data_tuple[0] all_sample_size = data_tuple[1] fid,fweight,label =data fid,fweight,label=map(Variable,[fid,fweight,label] optimizer = torch.optim.SGD(net.parameters(), lr=0.02) loss_func = torch.nn.CrossEntropyLoss() # the target label is NOT an one-hotted out = net(fid, fweight) optimizer.zero_grad() # clear gradients for next train loss = loss_func(out, label) loss.backward() # backpropagation, compute gradients optimizer.step() # apply gradients if batch_id % 2 == 0:#每两批次打印一次 # plot and show learning process prediction = torch.max(F.softmax(out), 1)[1] pred_y = prediction.data.numpy().squeeze() target_y = label.data.numpy() accuracy = sum(pred_y == target_y)/float(all_sample_size) print "epoch: %s, batch_id: %s, acc: %s" % (epoch, batch_id, accuracy)2.怎么使用模型?
    现在模型可以训练自己数据了,效果也提升了,那如何把模型的参数保存下来?
    print "epoch: %s, batch_id: %s, acc: %s" % (epoch, batch_id, accuracy) checkpoint = { 'model': net.state_dict(),#网络静态参数 'epoch': epoch, 'batch': batch_id, } model_name = 'epoch_%s_batch_id_%s_acc_%s.chkpt' % (epoch, batch_id, accuracy) model_path = join_path('./model', model_name) torch.save(checkpoint, model_path)拆分上面大文件出各个参数:

    import osimport sysimport torchimport torch.nn as nnimport randomfrom torch.autograd import Variablefrom os.path import join as join_pathimport torch.nn.functional as Fmodel_path = sys.argv[1]checkpoint = torch.load(model_path)model_dict = checkpoint['model']for k, v in model_dict.items(): print k model_sub_file_name = k model_sub_file_path = join_path('./dump', model_sub_file_name) f = open(model_sub_file_path, 'w') value = model_dict[k].cpu().numpy() value.dump(f) f.close()模型怎么用?加载、预测
    import sysimport numpy as npimport mathembedding_weight = np.load('dump/embedding.weight')hidden_weight = np.load('./dump/hidden.weight')hidden_bias = np.load('./dump/hidden.bias')out_weight = np.load('./dump/out.weight')out_bias = np.load('./dump/out.bias')emb_dim = embedding_weight.shape[1]def calc_score(fids, fweights): embedding = np.zeros(emb_dim) for id, weight in zip(fids, fweights): embedding += weight * embedding_weight[id] embedding_tanh = np.tanh(embedding)#对应相乘,第二维相加成10维向量 hidden = np.sum(np.multiply(hidden_weight, embedding_tanh), axis=1) + hidden_bias out = np.sum(np.multiply(out_weight, hidden), axis=1) + out_bias return outdef softmax(x): exp_x = np.exp(x) softmax_x = exp_x / np.sum(exp_x) return softmax_xfor line in sys.stdin: ss = line.strip().split(' ') label = ss[0].strip() fids = [] fweights = [] for f_s in ss[1:]: f, s = f_s.strip().split(':') fids.append(int(f)) fweights.append(float(s)) pred_label = softmax(calc_score(fids, fweights))[1] print label, pred_label3.怎么评估?auc
    cat auc.raw | sort -t$'\t' -k2g | awk -F'\t' '($1==-1){++x;a+=y}($1==1){++y}END{print 1.0 - a/(x*y)}'acc=0.827auc=0.842569acc=0.745auc=0.494206轮数、acc都影响着auc,数字仅供参考
    总结以上,是以二分类为例,从头演示了一遍神经网络,大家可再找一些0-9手写图片分类任务体验一下,这里总结下,模型拥有参数,所以涉及参数初始化,有了参数后涉及正向传递,分为拟合和泛化两部分建立好模型后,学习过程就是确定参数的过程,使用的是梯度下降法,定义一个误差函数loss function 来衡量模型预测值与真实值(label)之间的差距,通过不断降低差距来使模型预测值逼近真实值,更新参数的反向传递是利用对应的误差值来求得梯度(batch、epoch、learning rate 、shuffle),评估标准也根据任务类型和具体要求可以使用更多的指标。
    回归任务常用均方差MSE作为误差函数。评估可用均方差表示。
    分类任务常用交叉熵(cross entropy)和softmax配合使用。评估可用准确率表示。
    回归任务:

    借用知乎yjango末尾的一句话:
    但最重要的并非训练集的准确率,而是模型从未见过的测试集的准确率(泛化能力),正如高考,真正的目的是为了解出从未见过的高考题,而不是已经做过的练习题,学习的困难之处正是要用有限的样本训练出可以符合无限同类样本规律的模型。是有限同无限的对抗,你是否想过,为什么看了那么多的道理、听了那么多人生讲座,依然过不哈这一生,一个原因就在于演讲者向你展示的例子都是训练集的样本,他所归纳的方法可以很轻松的完美拟合这些样本,但未必对你将面临的新问题同样奏效,人的一生都在学习,都在通过观察有限的例子找出问题和答案的规律,中医是,玄学是,科学同样是,但科学,是当中最为可靠的一种。
    0 留言 2019-06-05 13:46:30 奖励18点积分
显示 0 到 15 ,共 15 条
eject