網站首頁 編程語言 正文
問題:讀取一個cpp文件,去除其中所有的單行注釋(//)和多行注釋(/**/),將去除注釋后的內容寫入一個新文件。
注意:
不能去除字符串中的任何字符,如 "asdf//gh/**/k" 需要原樣輸出
能夠識別轉義的雙引號(\")和非轉義的雙引號("),如:
- '\"'(單引號中的轉義雙引號字符)
- "asdf\""?(字符串中的轉義雙引號不能表示字符串結束)
- \"aaa"bcdef"?(同理,轉義雙引號不能表示字符串開始)
命令行參數為:
- 參數1:原文件名
- 參數2:新文件名
一、文件流
引入<fstream>頭文件,新建ifstream和ofstream對象,如果不能打開文件,則結束整個函數。?
#include <iostream>
#include <fstream>
using namespace std;
bool stripComment(string infile,string outfile){
ifstream ifs;
ifs.open(infile,ios::in);
if(!ifs.is_open()){
cout<<"fail to read!"<<endl;
return true;
}
ofstream ofs;
ofs.open(outfile,ios::out|ios::trunc);//寫文件,如果已經存在同名的文件,則刪除它創建新文件
if(!ofs.is_open()){
cout<<"fail to write!"<<endl;
return true;
}
ifs.close();
ofs.close();
}
int main(int argc,char** argv){
return 0;
}
二、具體邏輯
1.如何循環讀入字符
這是最關鍵的一步。我們利用ifstream對象的get()方法無條件地讀取文件流中的一個字符。什么叫無條件?這是與>>運算符相區分的,>>會自動跳過所有空白字符,也就是說,它讀不到空格、\t、\n,這不符合我們的需求。?
同時,ifstream對象有一個putback()方法,它就是將一個字符重新放回文件流的末尾,下一次get(),我們還會讀到它:這有時候很有用。
get(char c) 會把文件流的末尾一個字符取出,放到char型變量c中。這個函數的返回值可以被轉換成一個布爾值,表示流的狀態:如果流狀態正常,則返回true;如果流狀態不正常(bad,fail,讀到文件尾符號),返回false。
char temp1{};
while(ifs.get(temp1)){
ofs<<temp1;
}
最簡單的情況是,利用get()循環讀入字符并存入temp1變量中,再把temp1寫入新文件中。最終我們會得到與原文件一樣的副本。
2.處理單行和多行注釋
這兩種東西有同一個特點:都是以 / 開頭的。所以我們讀取到 / 這個字符時,就要小心一點:它是不是意味著我讀到了一個注釋?
我們可以在此基礎上再讀一個字符,存入另一個字符變量temp2中,看看它是什么情況:
- / :一定讀到了一個單行注釋
- * :一定讀到了一個多行注釋
- 其他字符:剛剛讀到的 / 只是一個普通的正斜杠
對于第三種情況,?我們這樣善后:
- 立刻輸出temp1(/)
- 把temp2放回文件流中等待下一次讀取,因為它可能有用(比如,是個雙引號)
- 跳轉到一開始的while循環
?對于第一種情況:
- 一直向后讀字符,直到讀到一個換行符\n
- 將這個換行符放回文件流
- 跳轉到一開始的while循環
第二種情況最麻煩,我們必須找一個辦法確定什么時候多行注釋才能結束。C++的語法規定, /* 與最近的 */ 之間是多行注釋,那么我們只需找最近的 */ 即可。
- while循環一直向后讀字符,直到找到一個 *
- 再讀一個字符,放入字符變量temp3中。
- temp3是一個 / ,恭喜!我們已經找到多行注釋的完整范圍,只要簡單地break掉當前的while循環,再跳轉到開始的while循環就可以。不用輸出任何東西。
- 不好,temp3不是 / ,這意味著我們沒有找到多行注釋的終止處。此時應該將temp3放回文件流,返回步驟1:這是很必要的,如果temp3正好是一個* ,它之后恰好是一個 / 呢?我們不能丟棄任何“可能有用”的字符!
while(ifs.get(temp1)){
//處理 "http://" 或 "/**/"
if(temp1=='/'){
checkStatus=true;
ifs.get(temp2);//再讀一個字符
if(temp2=='/'){//處理"http://"
checkStatus=false;
while(ifs.get(temp2)){//一直向后讀字符,直到讀到一個換行符\n
if(temp2=='\n') break;
}
ifs.putback('\n');//將換行符放回文件流
continue;//跳轉到一開始的while循環
}else if(temp2=='*'){//處理 "/**/"
checkStatus=true;
while(ifs.get(temp3)){
if(temp3=='*'){//找到一個*
ifs.get(temp4);
if(temp4=='/'){//找到一個/
checkStatus=false;
break;
}else{//沒找到/
ifs.putback(temp4);
continue;
}
}
}
continue;
}else{//只讀到了一個 /
checkStatus=false;
ifs.putback(temp2);//把temp2放回文件流中等待下一次讀取
}
}
ofs<<temp1;
}
3.注意字符串
我們在2中的操作會破壞字符串中的一些內容,比如:
處理前:"asdf//gh/**/k"
處理后(//被當成了單行注釋):"asdf
我們的需求是保留字符串原封不動輸出。
因此,當temp1沒有讀到 / 時(即沒有進入處理注釋的邏輯),temp1讀到了一個 " ,我們就要小心了:后面的內容是字符串,原樣輸出即可。
- 立刻輸出temp1。
- 循環讀入后面的字符,存入temp2中,立即輸出temp2。
- 檢查剛剛輸出的temp2是不是 " ,如果是,結束當前循環并跳轉到一開始的while循環
while(ifs.get(temp1)){
//處理 "http://" 或 "/**/"
//省略...
//處理字符串
if(temp1=='\"'){
ofs<<temp1;//立刻輸出第一個雙引號
checkStatus=true;
while(ifs.get(temp2)){
ofs<<temp2;//立刻輸出temp2
if(temp2=='\"'){//temp2是結尾的雙引號
checkStatus=false;
break;
}
}
continue;//返回一開始的while循環
}
ofs<<temp1;
}
4.注意轉義雙引號
以上的兩塊邏輯能解決一般問題,但在以下的例子中會出錯:
char c='\"';
const char* p1 = "sdjksd\"\\d//fj/*kdhjk\"dsfjl*/dks";
int main()
{
cout << "http://The number of queens (4 -- 12) :// " ;
}
最后的結果是:
char c='\"';
const char* p1 = "sdjksd\"\\d//fj/*kdhjk\"dsfjl*/dks";
int main()
{
cout << "
}
為什么呢?其實是轉義雙引號(\")在作怪,因為我們目前的邏輯將轉義雙引號與表示字符串開頭與結尾的普通雙引號混為一談,導致字符串的邊界出現混亂。
以下紅色部分表示程序所認為的字符串,藍色部分表示程序認為是注釋的地方():
可以看出這個問題挺嚴重的,首要之急就是區分轉義雙引號和普通雙引號。
C++的文件流是逐個字符讀取的,也就是說,\" 在get()時,第一次會得到 \ ,第二次會得到 " 。
另一個有用的信息就是,C++的字符串邊界永遠是普通雙引號,而不是轉義雙引號。
1>防止轉義雙引號作為字符串開頭
- 當沒有進入處理注釋的邏輯時,讀到了一個 \
- 立刻輸出 \ 本身
- 讀取下一個字符存入temp2中
- 立刻輸出temp2
- 跳轉到一開始的while循環
為什么這樣做呢?分析一下,因為 \ 與它后面的字符已經被轉義(它們是一個整體),\ 后面的字符不能被認為是代碼語法的一部分。比如:\ 之后的 " 不能被認為是普通的雙引號,\ 之后的 / 也不能被認為是一個代表注釋開頭的 /。反斜杠后面的字符從語法上來說應該直接輸出。
注:處理注釋時不需這種邏輯,注釋里的 \ 沒有什么特殊的意義,和普通字符地位一樣。它不能影響后面那個字符的含義。
/*aaaaa\*/
仍然表示一個多行注釋
2>防止轉義雙引號作為字符串結尾
要在字符串的輸出邏輯中增加檢查機制。
- 剛剛輸出的字符(temp2)如果是一個 \
- 讀取下一個字符并直接輸出
- 繼續執行當前的while輸出循環,直到temp2是一個雙引號為止?
while(ifs.get(temp1)){
//處理 "http://" 或 "/**/"
//...省略
//防止\"作為字符串開頭
if(temp1=='\\'){
checkStatus=true;
ofs<<temp1;//直接輸出\
//如果\后面有字符,直接輸出\后面的字符
if(ifs.get(temp2)){
ofs<<temp2;
checkStatus=false;
}
continue;//跳轉到一開始的while循環
}
//處理字符串
if(temp1=='\"'){
ofs<<temp1;
checkStatus=true;
while(ifs.get(temp2)){
ofs<<temp2;
//如果剛剛輸出的是一個\
if(temp2=='\\'){
if(ifs.get(temp3)) ofs<<temp3;//直接輸出后一個字符
continue;//繼續當前的while循環
}
if(temp2=='\"'){//輸出了一個普通雙引號,表示字符串結束
checkStatus=false;
break;
}
}
continue;
}
ofs<<temp1;
}
5.增加簡單的查錯功能
當我們發現了 /* 或 " 時,這意味著我們開始進行注釋邊界的確定或字符串邊界的確定。當發現了一個 \ 之后,\ 后面理應有一個字符。
如果我們:
- 找到了 "?卻始終未找到對應的 "
- 找到了 /* 卻始終沒有找到 */
- 找到了 \ 后面卻無字符
這三種情況意味著原文件的語法一定有問題,我們用一個變量checkStatus表示。當開始進行"尋找"過程,checkStatus被設為true。當尋找結束后,變量的值被設為false。如果文件流結束,此變量依然為true,那么應提醒用戶,文件的語法有問題。
完整代碼如下:
#include <iostream>
#include <fstream>
using namespace std;
bool stripComment(string infile,string outfile){
ifstream ifs;
ifs.open(infile,ios::in);
if(!ifs.is_open()){
cout<<"fail to read!"<<endl;
return true;
}
ofstream ofs;
ofs.open(outfile,ios::out|ios::trunc);
if(!ofs.is_open()){
cout<<"fail to write!"<<endl;
return true;
}
bool checkStatus=false;
char temp1{};
char temp2{};
char temp3{};
while(ifs.get(temp1)){
//handle "http://" OR "/**/"
if(temp1=='/'){
checkStatus=true;
ifs.get(temp2);
if(temp2=='/'){//handle "http://"
checkStatus=false;
while(ifs.get(temp2)){
if(temp2=='\n') break;
}
ifs.putback('\n');
continue;
}else if(temp2=='*'){//handle "/**/"
checkStatus=true;
while(ifs.get(temp2)){
if(temp2=='*'){
ifs.get(temp3);
if(temp3=='/'){
checkStatus=false;
break;
}else{
ifs.putback(temp3);
continue;
}
}
}
continue;
}else{//only read a '/'
checkStatus=false;
ifs.putback(temp2);
}
}
//handle possible '\"'
if(temp1=='\\'){
checkStatus=true;
ofs<<temp1;
//output char after '\'
if(ifs.get(temp2)){
ofs<<temp2;
checkStatus=false;
}
continue;
}
//handle true ""
if(temp1=='\"'){
ofs<<temp1;
checkStatus=true;
while(ifs.get(temp2)){
ofs<<temp2;
//handle things after '\'
if(temp2=='\\'){
if(ifs.get(temp3)) ofs<<temp3;
continue;
}
if(temp2=='\"'){
checkStatus=false;
break;
}
}
continue;
}
ofs<<temp1;
}
ifs.close();
ofs.close();
return checkStatus;
}
int main(int argc,char** argv){
if(argc!=3){//檢查是否輸入了兩個文件名
cout<<"argument invalid!"<<endl;
return -1;
}
bool res=stripComment(argv[1],argv[2]);//去除注釋
if(res==true){//文件語法有錯
cout<<"maybe wrong."<<endl;
}
return 0;
}
三、正則實現(Java)
這是題外話,但其實正則表達式也能實現這一功能。
由于C++的正則似乎不支持后發斷言,所以使用了Java的正則類。
主要思路:
- 讀取待去除注釋的文本,存入一個字符串中。
- 創建兩個正則表達式,一個匹配注釋和字符串,另一個只能匹配注釋。
- 對該字符串循環匹配第一個正則表達式,得到結果str。
- 若str符合第二個正則表達式,則記下str在原字符串中的起始下標和結束下標,將二者都存入ArrayList對象中。
- 匹配結束后,遍歷原字符串,如果字符下標位于”存儲下來的注釋區域的起始和結束下標“之間,就不輸出這些字符。
package project1100;
import java.util.ArrayList;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class mainTest {
public static void main(String[] args) {
String str2=
"const char *qq=\"/**/\";\n" +
"const char *pp=/**\"\\\"007*/\"/**/\";\n" +
"const char *rr=/**'\\'007*/\"/**/\"\"http://\";";
//匹配注釋和字符串
Pattern pattern1=Pattern.compile("(/((/.*)|(\\*[\\S\\s]*?\\*/)))|((?<!\\\\)(\"([\\S\\s]*?)[^\\\\]\"))");
//只能匹配注釋
Pattern pattern2=Pattern.compile("(/((/.*)|(\\*[\\S\\s]*?\\*/)))");
//用于存儲注釋區域下標范圍的容器
ArrayList<Integer> arrayList=new ArrayList<Integer>();
Matcher matcher1=pattern1.matcher(str2);
//匹配注釋和字符串
while(matcher1.find()){
//是注釋,下標存入容器中
if(pattern2.matcher(matcher1.group(0)).matches()) {
arrayList.add(matcher1.start(0));
arrayList.add(matcher1.end(0));
}
}
//選擇輸出
label:for(int i=0;i<str2.length();i++){
for(int j=0;j<arrayList.size()/2;j++){
if(i>=arrayList.get(j*2)&&i<arrayList.get(j*2+1)){
continue label;
}
}
System.out.print(str2.charAt(i));
}
/*結果
const char *qq="/**/";
const char *pp="/**/";
const char *rr="/**/""http://";
*/
}
}
?圖解:
?可能仍有疏漏,請各位大佬指正。
結語:
去除代碼中的注釋,看似簡單,實則要考慮諸多情況。這是一個極其考驗思維細致度的問題,希望大家都能考慮到每一種情況,找到自己最好理解的思路。
原文鏈接:https://blog.csdn.net/m0_62505136/article/details/121548141
相關推薦
- 2022-12-14 C#中委托和事件的區別詳解_C#教程
- 2022-12-12 Android?Cocos?Creator游戲開發平臺打包優化實現方案_Android
- 2022-12-06 .net程序開發IOC控制反轉和DI依賴注入詳解_ASP.NET
- 2022-07-13 Android自定義View實現簡易畫板功能_Android
- 2022-08-13 Linux管理員root密碼忘記了怎么辦?
- 2023-01-12 Python讀取及保存mat文件的注意事項說明_python
- 2023-03-22 Golang創建構造函數的方法超詳細講解_Golang
- 2023-01-31 C#實現偽裝文件夾功能_C#教程
- 最近更新
-
- window11 系統安裝 yarn
- 超詳細win安裝深度學習環境2025年最新版(
- Linux 中運行的top命令 怎么退出?
- MySQL 中decimal 的用法? 存儲小
- get 、set 、toString 方法的使
- @Resource和 @Autowired注解
- Java基礎操作-- 運算符,流程控制 Flo
- 1. Int 和Integer 的區別,Jav
- spring @retryable不生效的一種
- Spring Security之認證信息的處理
- Spring Security之認證過濾器
- Spring Security概述快速入門
- Spring Security之配置體系
- 【SpringBoot】SpringCache
- Spring Security之基于方法配置權
- redisson分布式鎖中waittime的設
- maven:解決release錯誤:Artif
- restTemplate使用總結
- Spring Security之安全異常處理
- MybatisPlus優雅實現加密?
- Spring ioc容器與Bean的生命周期。
- 【探索SpringCloud】服務發現-Nac
- Spring Security之基于HttpR
- Redis 底層數據結構-簡單動態字符串(SD
- arthas操作spring被代理目標對象命令
- Spring中的單例模式應用詳解
- 聊聊消息隊列,發送消息的4種方式
- bootspring第三方資源配置管理
- GIT同步修改后的遠程分支